diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index caab752..b5a27cd 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -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 diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index cddbd62..360ebaa 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -183,6 +183,7 @@ const updatePassword = async ( } dbUser.password = User.hashPassword(newPassword) + dbUser.needsToUpdatePassword = false await dbUser.save() } diff --git a/api/src/controllers/authConfig.ts b/api/src/controllers/authConfig.ts index cf13fd6..2ca98d7 100644 --- a/api/src/controllers/authConfig.ts +++ b/api/src/controllers/authConfig.ts @@ -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) diff --git a/api/src/controllers/session.ts b/api/src/controllers/session.ts index 0571529..f0cd049 100644 --- a/api/src/controllers/session.ts +++ b/api/src/controllers/session.ts @@ -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 { + ): Promise { 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 }) diff --git a/api/src/controllers/web.ts b/api/src/controllers/web.ts index e496235..9ed18ca 100644 --- a/api/src/controllers/web.ts +++ b/api/src/controllers/web.ts @@ -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 } } } diff --git a/api/src/middlewares/authenticateToken.ts b/api/src/middlewares/authenticateToken.ts index 901c6e7..f0da345 100644 --- a/api/src/middlewares/authenticateToken.ts +++ b/api/src/middlewares/authenticateToken.ts @@ -81,7 +81,8 @@ const authenticateToken = async ( username: 'desktopModeUsername', displayName: 'desktopModeDisplayName', isAdmin: true, - isActive: true + isActive: true, + needsToUpdatePassword: false } req.accessToken = 'desktopModeAccessToken' return next() diff --git a/api/src/middlewares/desktop.ts b/api/src/middlewares/desktop.ts index b2935fd..3b352f4 100644 --- a/api/src/middlewares/desktop.ts +++ b/api/src/middlewares/desktop.ts @@ -33,5 +33,6 @@ export const desktopUser: RequestUser = { username: userInfo().username, displayName: userInfo().username, isAdmin: true, - isActive: true + isActive: true, + needsToUpdatePassword: false } diff --git a/api/src/model/User.ts b/api/src/model/User.ts index e63ba6f..a9edf1b 100644 --- a/api/src/model/User.ts +++ b/api/src/model/User.ts @@ -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({ type: Boolean, default: true }, + needsToUpdatePassword: { + type: Boolean, + default: true + }, autoExec: { type: String }, diff --git a/api/src/types/RequestUser.ts b/api/src/types/RequestUser.ts index e85507f..fe4cdab 100644 --- a/api/src/types/RequestUser.ts +++ b/api/src/types/RequestUser.ts @@ -5,5 +5,6 @@ export interface RequestUser { displayName: string isAdmin: boolean isActive: boolean + needsToUpdatePassword: boolean autoExec?: string } diff --git a/api/src/utils/isPublicRoute.ts b/api/src/utils/isPublicRoute.ts index 0b121ee..178721f 100644 --- a/api/src/utils/isPublicRoute.ts +++ b/api/src/utils/isPublicRoute.ts @@ -27,5 +27,6 @@ export const publicUser: RequestUser = { username: 'publicUser', displayName: 'Public User', isAdmin: false, - isActive: true + isActive: true, + needsToUpdatePassword: false } diff --git a/api/src/utils/verifyTokenInDB.ts b/api/src/utils/verifyTokenInDB.ts index 83c4a26..9ee12c6 100644 --- a/api/src/utils/verifyTokenInDB.ts +++ b/api/src/utils/verifyTokenInDB.ts @@ -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 diff --git a/web/src/App.tsx b/web/src/App.tsx index e0a7d3e..066e419 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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 ( + + +
+ + } /> + + + + + ) + } + return ( diff --git a/web/src/components/login.tsx b/web/src/components/login.tsx index c679a8d..a458b88 100644 --- a/web/src/components/login.tsx +++ b/web/src/components/login.tsx @@ -32,6 +32,7 @@ const Login = () => { appContext.setDisplayName?.(user.displayName) appContext.setIsAdmin?.(user.isAdmin) appContext.setLoggedIn?.(loggedIn) + appContext.setNeedsToUpdatePassword?.(user.needsToUpdatePassword) } } diff --git a/web/src/components/passwordModal.tsx b/web/src/components/passwordModal.tsx index f9c1ee8..28e896b 100644 --- a/web/src/components/passwordModal.tsx +++ b/web/src/components/passwordModal.tsx @@ -108,7 +108,7 @@ type PasswordInputProps = { handleBlur?: () => void } -const PasswordInput = ({ +export const PasswordInput = ({ label, password, setPassword, diff --git a/web/src/components/updatePassword.tsx b/web/src/components/updatePassword.tsx new file mode 100644 index 0000000..12ceaf8 --- /dev/null +++ b/web/src/components/updatePassword.tsx @@ -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 ? ( + + ) : ( + :not(style)': { m: 1, width: '25ch' } + }} + > + +

Welcome to SASjs Server!

+

+ This is your first time login to SASjs server. Therefore, you need to + update your password. +

+ + + +
+ ) +} + +export default UpdatePassword diff --git a/web/src/context/appContext.tsx b/web/src/context/appContext.tsx index b43aad2..6c34e61 100644 --- a/web/src/context/appContext.tsx +++ b/web/src/context/appContext.tsx @@ -25,6 +25,8 @@ interface AppContextProps { checkingSession: boolean loggedIn: boolean setLoggedIn: Dispatch> | null + needsToUpdatePassword: boolean + setNeedsToUpdatePassword: Dispatch> | null userId: number setUserId: Dispatch> | null username: string @@ -42,6 +44,8 @@ export const AppContext = createContext({ 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,