mirror of
https://github.com/sasjs/server.git
synced 2025-12-12 11:54:35 +00:00
feat: ask for updated password on first login
This commit is contained in:
@@ -534,6 +534,27 @@ components:
|
|||||||
- setting
|
- setting
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
SessionResponse:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
displayName:
|
||||||
|
type: string
|
||||||
|
isAdmin:
|
||||||
|
type: boolean
|
||||||
|
needsToUpdatePassword:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- username
|
||||||
|
- displayName
|
||||||
|
- isAdmin
|
||||||
|
- needsToUpdatePassword
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
ExecutePostRequestPayload:
|
ExecutePostRequestPayload:
|
||||||
properties:
|
properties:
|
||||||
_program:
|
_program:
|
||||||
@@ -1724,7 +1745,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UserResponse'
|
$ref: '#/components/schemas/SessionResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
|
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
|
||||||
@@ -1821,7 +1842,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
properties:
|
properties:
|
||||||
user: {properties: {isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [isAdmin, displayName, username, id], type: object}
|
user: {properties: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object}
|
||||||
loggedIn: {type: boolean}
|
loggedIn: {type: boolean}
|
||||||
required:
|
required:
|
||||||
- user
|
- user
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ const updatePassword = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbUser.password = User.hashPassword(newPassword)
|
dbUser.password = User.hashPassword(newPassword)
|
||||||
|
dbUser.needsToUpdatePassword = false
|
||||||
await dbUser.save()
|
await dbUser.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ const synchroniseWithLDAP = async () => {
|
|||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
password: hashPassword,
|
password: hashPassword,
|
||||||
authProvider: AuthProviderType.LDAP
|
authProvider: AuthProviderType.LDAP,
|
||||||
|
needsToUpdatePassword: false
|
||||||
})
|
})
|
||||||
|
|
||||||
importedUsers.push(user)
|
importedUsers.push(user)
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import express from 'express'
|
|||||||
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
|
|
||||||
|
interface SessionResponse extends UserResponse {
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/session')
|
@Route('SASjsApi/session')
|
||||||
@Tags('Session')
|
@Tags('Session')
|
||||||
@@ -19,7 +23,7 @@ export class SessionController {
|
|||||||
@Get('/')
|
@Get('/')
|
||||||
public async session(
|
public async session(
|
||||||
@Request() request: express.Request
|
@Request() request: express.Request
|
||||||
): Promise<UserResponse> {
|
): Promise<SessionResponse> {
|
||||||
return session(request)
|
return session(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,5 +32,6 @@ const session = (req: express.Request) => ({
|
|||||||
id: req.user!.userId,
|
id: req.user!.userId,
|
||||||
username: req.user!.username,
|
username: req.user!.username,
|
||||||
displayName: req.user!.displayName,
|
displayName: req.user!.displayName,
|
||||||
isAdmin: req.user!.isAdmin
|
isAdmin: req.user!.isAdmin,
|
||||||
|
needsToUpdatePassword: req.user!.needsToUpdatePassword
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ const login = async (
|
|||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isActive: user.isActive,
|
isActive: user.isActive,
|
||||||
autoExec: user.autoExec
|
autoExec: user.autoExec,
|
||||||
|
needsToUpdatePassword: user.needsToUpdatePassword
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -113,7 +114,8 @@ const login = async (
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
isAdmin: user.isAdmin
|
isAdmin: user.isAdmin,
|
||||||
|
needsToUpdatePassword: user.needsToUpdatePassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ const authenticateToken = async (
|
|||||||
username: 'desktopModeUsername',
|
username: 'desktopModeUsername',
|
||||||
displayName: 'desktopModeDisplayName',
|
displayName: 'desktopModeDisplayName',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
needsToUpdatePassword: false
|
||||||
}
|
}
|
||||||
req.accessToken = 'desktopModeAccessToken'
|
req.accessToken = 'desktopModeAccessToken'
|
||||||
return next()
|
return next()
|
||||||
|
|||||||
@@ -33,5 +33,6 @@ export const desktopUser: RequestUser = {
|
|||||||
username: userInfo().username,
|
username: userInfo().username,
|
||||||
displayName: userInfo().username,
|
displayName: userInfo().username,
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
needsToUpdatePassword: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ interface IUserDocument extends UserPayload, Document {
|
|||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
autoExec: string
|
autoExec: string
|
||||||
groups: Schema.Types.ObjectId[]
|
groups: Schema.Types.ObjectId[]
|
||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
@@ -81,6 +82,10 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
needsToUpdatePassword: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
autoExec: {
|
autoExec: {
|
||||||
type: String
|
type: String
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export interface RequestUser {
|
|||||||
displayName: string
|
displayName: string
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
autoExec?: string
|
autoExec?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,5 +27,6 @@ export const publicUser: RequestUser = {
|
|||||||
username: 'publicUser',
|
username: 'publicUser',
|
||||||
displayName: 'Public User',
|
displayName: 'Public User',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
needsToUpdatePassword: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const fetchLatestAutoExec = async (
|
|||||||
displayName: dbUser.displayName,
|
displayName: dbUser.displayName,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isActive: dbUser.isActive,
|
isActive: dbUser.isActive,
|
||||||
|
needsToUpdatePassword: dbUser.needsToUpdatePassword,
|
||||||
autoExec: dbUser.autoExec
|
autoExec: dbUser.autoExec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ export const verifyTokenInDB = async (
|
|||||||
displayName: dbUser.displayName,
|
displayName: dbUser.displayName,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isActive: dbUser.isActive,
|
isActive: dbUser.isActive,
|
||||||
|
needsToUpdatePassword: dbUser.needsToUpdatePassword,
|
||||||
autoExec: dbUser.autoExec
|
autoExec: dbUser.autoExec
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Header from './components/header'
|
|||||||
import Home from './components/home'
|
import Home from './components/home'
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
import Settings from './containers/Settings'
|
import Settings from './containers/Settings'
|
||||||
|
import UpdatePassword from './components/updatePassword'
|
||||||
|
|
||||||
import { AppContext } from './context/appContext'
|
import { AppContext } from './context/appContext'
|
||||||
import AuthCode from './containers/AuthCode'
|
import AuthCode from './containers/AuthCode'
|
||||||
@@ -29,6 +30,20 @@ function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appContext.needsToUpdatePassword) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<HashRouter>
|
||||||
|
<Header />
|
||||||
|
<Routes>
|
||||||
|
<Route path="*" element={<UpdatePassword />} />
|
||||||
|
</Routes>
|
||||||
|
<ToastContainer />
|
||||||
|
</HashRouter>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const Login = () => {
|
|||||||
appContext.setDisplayName?.(user.displayName)
|
appContext.setDisplayName?.(user.displayName)
|
||||||
appContext.setIsAdmin?.(user.isAdmin)
|
appContext.setIsAdmin?.(user.isAdmin)
|
||||||
appContext.setLoggedIn?.(loggedIn)
|
appContext.setLoggedIn?.(loggedIn)
|
||||||
|
appContext.setNeedsToUpdatePassword?.(user.needsToUpdatePassword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ type PasswordInputProps = {
|
|||||||
handleBlur?: () => void
|
handleBlur?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PasswordInput = ({
|
export const PasswordInput = ({
|
||||||
label,
|
label,
|
||||||
password,
|
password,
|
||||||
setPassword,
|
setPassword,
|
||||||
|
|||||||
109
web/src/components/updatePassword.tsx
Normal file
109
web/src/components/updatePassword.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Box, CssBaseline, Button, CircularProgress } from '@mui/material'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { PasswordInput } from './passwordModal'
|
||||||
|
|
||||||
|
import { AppContext } from '../context/appContext'
|
||||||
|
|
||||||
|
const UpdatePassword = () => {
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('')
|
||||||
|
const [newPassword, setNewPassword] = useState('')
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
const [errorText, setErrorText] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
currentPassword.length > 0 &&
|
||||||
|
newPassword.length > 0 &&
|
||||||
|
newPassword === currentPassword
|
||||||
|
) {
|
||||||
|
setErrorText('New password should be different to current password.')
|
||||||
|
setHasError(true)
|
||||||
|
} else if (newPassword.length >= 6) {
|
||||||
|
setErrorText('')
|
||||||
|
setHasError(false)
|
||||||
|
}
|
||||||
|
}, [currentPassword, newPassword])
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
setErrorText('Password length should be at least 6 characters.')
|
||||||
|
setHasError(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: any) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (hasError || !currentPassword || !newPassword) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.patch(`/SASjsApi/auth/updatePassword`, {
|
||||||
|
currentPassword,
|
||||||
|
newPassword
|
||||||
|
})
|
||||||
|
.then((res: any) => {
|
||||||
|
appContext.setNeedsToUpdatePassword?.(false)
|
||||||
|
toast.success('Password updated', {
|
||||||
|
theme: 'dark',
|
||||||
|
position: toast.POSITION.BOTTOM_RIGHT
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed: ' + err.response?.data || err.text, {
|
||||||
|
theme: 'dark',
|
||||||
|
position: toast.POSITION.BOTTOM_RIGHT
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLoading ? (
|
||||||
|
<CircularProgress
|
||||||
|
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
className="main"
|
||||||
|
component="form"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
sx={{
|
||||||
|
'& > :not(style)': { m: 1, width: '25ch' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CssBaseline />
|
||||||
|
<h2>Welcome to SASjs Server!</h2>
|
||||||
|
<p style={{ width: 'auto' }}>
|
||||||
|
This is your first time login to SASjs server. Therefore, you need to
|
||||||
|
update your password.
|
||||||
|
</p>
|
||||||
|
<PasswordInput
|
||||||
|
label="Current Password"
|
||||||
|
password={currentPassword}
|
||||||
|
setPassword={setCurrentPassword}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="New Password"
|
||||||
|
password={newPassword}
|
||||||
|
setPassword={setNewPassword}
|
||||||
|
hasError={hasError}
|
||||||
|
errorText={errorText}
|
||||||
|
handleBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outlined"
|
||||||
|
disabled={hasError || !currentPassword || !newPassword}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdatePassword
|
||||||
@@ -25,6 +25,8 @@ interface AppContextProps {
|
|||||||
checkingSession: boolean
|
checkingSession: boolean
|
||||||
loggedIn: boolean
|
loggedIn: boolean
|
||||||
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
|
setNeedsToUpdatePassword: Dispatch<SetStateAction<boolean>> | null
|
||||||
userId: number
|
userId: number
|
||||||
setUserId: Dispatch<SetStateAction<number>> | null
|
setUserId: Dispatch<SetStateAction<number>> | null
|
||||||
username: string
|
username: string
|
||||||
@@ -42,6 +44,8 @@ export const AppContext = createContext<AppContextProps>({
|
|||||||
checkingSession: false,
|
checkingSession: false,
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
setLoggedIn: null,
|
setLoggedIn: null,
|
||||||
|
needsToUpdatePassword: false,
|
||||||
|
setNeedsToUpdatePassword: null,
|
||||||
userId: 0,
|
userId: 0,
|
||||||
setUserId: null,
|
setUserId: null,
|
||||||
username: '',
|
username: '',
|
||||||
@@ -59,6 +63,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
const { children } = props
|
const { children } = props
|
||||||
const [checkingSession, setCheckingSession] = useState(false)
|
const [checkingSession, setCheckingSession] = useState(false)
|
||||||
const [loggedIn, setLoggedIn] = useState(false)
|
const [loggedIn, setLoggedIn] = useState(false)
|
||||||
|
const [needsToUpdatePassword, setNeedsToUpdatePassword] = useState(false)
|
||||||
const [userId, setUserId] = useState(0)
|
const [userId, setUserId] = useState(0)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [displayName, setDisplayName] = useState('')
|
const [displayName, setDisplayName] = useState('')
|
||||||
@@ -79,6 +84,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
setDisplayName(data.displayName)
|
setDisplayName(data.displayName)
|
||||||
setIsAdmin(data.isAdmin)
|
setIsAdmin(data.isAdmin)
|
||||||
setLoggedIn(true)
|
setLoggedIn(true)
|
||||||
|
setNeedsToUpdatePassword(data.needsToUpdatePassword)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoggedIn(false)
|
setLoggedIn(false)
|
||||||
@@ -120,6 +126,8 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
checkingSession,
|
checkingSession,
|
||||||
loggedIn,
|
loggedIn,
|
||||||
setLoggedIn,
|
setLoggedIn,
|
||||||
|
needsToUpdatePassword,
|
||||||
|
setNeedsToUpdatePassword,
|
||||||
userId,
|
userId,
|
||||||
setUserId,
|
setUserId,
|
||||||
username,
|
username,
|
||||||
|
|||||||
Reference in New Issue
Block a user