mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
feat: ask for updated password on first login
This commit is contained in:
@@ -534,6 +534,27 @@ components:
|
||||
- setting
|
||||
type: object
|
||||
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:
|
||||
properties:
|
||||
_program:
|
||||
@@ -1724,7 +1745,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserResponse'
|
||||
$ref: '#/components/schemas/SessionResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
|
||||
@@ -1821,7 +1842,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
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}
|
||||
required:
|
||||
- user
|
||||
|
||||
@@ -183,6 +183,7 @@ const updatePassword = async (
|
||||
}
|
||||
|
||||
dbUser.password = User.hashPassword(newPassword)
|
||||
dbUser.needsToUpdatePassword = false
|
||||
await dbUser.save()
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,8 @@ const synchroniseWithLDAP = async () => {
|
||||
displayName: user.displayName,
|
||||
username: user.username,
|
||||
password: hashPassword,
|
||||
authProvider: AuthProviderType.LDAP
|
||||
authProvider: AuthProviderType.LDAP,
|
||||
needsToUpdatePassword: false
|
||||
})
|
||||
|
||||
importedUsers.push(user)
|
||||
|
||||
@@ -2,6 +2,10 @@ import express from 'express'
|
||||
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
||||
import { UserResponse } from './user'
|
||||
|
||||
interface SessionResponse extends UserResponse {
|
||||
needsToUpdatePassword: boolean
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/session')
|
||||
@Tags('Session')
|
||||
@@ -19,7 +23,7 @@ export class SessionController {
|
||||
@Get('/')
|
||||
public async session(
|
||||
@Request() request: express.Request
|
||||
): Promise<UserResponse> {
|
||||
): Promise<SessionResponse> {
|
||||
return session(request)
|
||||
}
|
||||
}
|
||||
@@ -28,5 +32,6 @@ const session = (req: express.Request) => ({
|
||||
id: req.user!.userId,
|
||||
username: req.user!.username,
|
||||
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,
|
||||
isAdmin: user.isAdmin,
|
||||
isActive: user.isActive,
|
||||
autoExec: user.autoExec
|
||||
autoExec: user.autoExec,
|
||||
needsToUpdatePassword: user.needsToUpdatePassword
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -113,7 +114,8 @@ const login = async (
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
isAdmin: user.isAdmin
|
||||
isAdmin: user.isAdmin,
|
||||
needsToUpdatePassword: user.needsToUpdatePassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,8 @@ const authenticateToken = async (
|
||||
username: 'desktopModeUsername',
|
||||
displayName: 'desktopModeDisplayName',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
needsToUpdatePassword: false
|
||||
}
|
||||
req.accessToken = 'desktopModeAccessToken'
|
||||
return next()
|
||||
|
||||
@@ -33,5 +33,6 @@ export const desktopUser: RequestUser = {
|
||||
username: userInfo().username,
|
||||
displayName: userInfo().username,
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
needsToUpdatePassword: false
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ interface IUserDocument extends UserPayload, Document {
|
||||
id: number
|
||||
isAdmin: boolean
|
||||
isActive: boolean
|
||||
needsToUpdatePassword: boolean
|
||||
autoExec: string
|
||||
groups: Schema.Types.ObjectId[]
|
||||
tokens: [{ [key: string]: string }]
|
||||
@@ -81,6 +82,10 @@ const userSchema = new Schema<IUserDocument>({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
needsToUpdatePassword: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoExec: {
|
||||
type: String
|
||||
},
|
||||
|
||||
@@ -5,5 +5,6 @@ export interface RequestUser {
|
||||
displayName: string
|
||||
isAdmin: boolean
|
||||
isActive: boolean
|
||||
needsToUpdatePassword: boolean
|
||||
autoExec?: string
|
||||
}
|
||||
|
||||
@@ -27,5 +27,6 @@ export const publicUser: RequestUser = {
|
||||
username: 'publicUser',
|
||||
displayName: 'Public User',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
needsToUpdatePassword: false
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const fetchLatestAutoExec = async (
|
||||
displayName: dbUser.displayName,
|
||||
isAdmin: dbUser.isAdmin,
|
||||
isActive: dbUser.isActive,
|
||||
needsToUpdatePassword: dbUser.needsToUpdatePassword,
|
||||
autoExec: dbUser.autoExec
|
||||
}
|
||||
}
|
||||
@@ -41,6 +42,7 @@ export const verifyTokenInDB = async (
|
||||
displayName: dbUser.displayName,
|
||||
isAdmin: dbUser.isAdmin,
|
||||
isActive: dbUser.isActive,
|
||||
needsToUpdatePassword: dbUser.needsToUpdatePassword,
|
||||
autoExec: dbUser.autoExec
|
||||
}
|
||||
: undefined
|
||||
|
||||
@@ -8,6 +8,7 @@ import Header from './components/header'
|
||||
import Home from './components/home'
|
||||
import Studio from './containers/Studio'
|
||||
import Settings from './containers/Settings'
|
||||
import UpdatePassword from './components/updatePassword'
|
||||
|
||||
import { AppContext } from './context/appContext'
|
||||
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 (
|
||||
<ThemeProvider theme={theme}>
|
||||
<HashRouter>
|
||||
|
||||
@@ -32,6 +32,7 @@ const Login = () => {
|
||||
appContext.setDisplayName?.(user.displayName)
|
||||
appContext.setIsAdmin?.(user.isAdmin)
|
||||
appContext.setLoggedIn?.(loggedIn)
|
||||
appContext.setNeedsToUpdatePassword?.(user.needsToUpdatePassword)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ type PasswordInputProps = {
|
||||
handleBlur?: () => void
|
||||
}
|
||||
|
||||
const PasswordInput = ({
|
||||
export const PasswordInput = ({
|
||||
label,
|
||||
password,
|
||||
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
|
||||
loggedIn: boolean
|
||||
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
||||
needsToUpdatePassword: boolean
|
||||
setNeedsToUpdatePassword: Dispatch<SetStateAction<boolean>> | null
|
||||
userId: number
|
||||
setUserId: Dispatch<SetStateAction<number>> | null
|
||||
username: string
|
||||
@@ -42,6 +44,8 @@ export const AppContext = createContext<AppContextProps>({
|
||||
checkingSession: false,
|
||||
loggedIn: false,
|
||||
setLoggedIn: null,
|
||||
needsToUpdatePassword: false,
|
||||
setNeedsToUpdatePassword: null,
|
||||
userId: 0,
|
||||
setUserId: null,
|
||||
username: '',
|
||||
@@ -59,6 +63,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
const { children } = props
|
||||
const [checkingSession, setCheckingSession] = useState(false)
|
||||
const [loggedIn, setLoggedIn] = useState(false)
|
||||
const [needsToUpdatePassword, setNeedsToUpdatePassword] = useState(false)
|
||||
const [userId, setUserId] = useState(0)
|
||||
const [username, setUsername] = useState('')
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
@@ -79,6 +84,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
setDisplayName(data.displayName)
|
||||
setIsAdmin(data.isAdmin)
|
||||
setLoggedIn(true)
|
||||
setNeedsToUpdatePassword(data.needsToUpdatePassword)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoggedIn(false)
|
||||
@@ -120,6 +126,8 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
checkingSession,
|
||||
loggedIn,
|
||||
setLoggedIn,
|
||||
needsToUpdatePassword,
|
||||
setNeedsToUpdatePassword,
|
||||
userId,
|
||||
setUserId,
|
||||
username,
|
||||
|
||||
Reference in New Issue
Block a user