From b27d68414540c401d0aba7620a92f6c38635c854 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 17 Nov 2022 23:03:33 +0500 Subject: [PATCH 1/7] chore: use process.logger instead of condole.log --- api/src/app-modules/configureCors.ts | 2 +- api/src/app-modules/configureLogger.ts | 2 +- api/src/app.ts | 4 ++-- api/src/controllers/internal/Session.ts | 9 ++++++--- api/src/controllers/internal/processProgram.ts | 4 ++-- api/src/controllers/mock-sas9.ts | 6 +++--- api/src/routes/appStream/index.ts | 2 +- api/src/server.ts | 6 +++--- api/src/utils/appStreamConfig.ts | 2 +- api/src/utils/connectDB.ts | 2 +- api/src/utils/copySASjsCore.ts | 4 ++-- api/src/utils/createWeboutSasFile.ts | 2 +- api/src/utils/getCertificates.ts | 6 +++--- api/src/utils/parseHelmetConfig.ts | 4 ++-- api/src/utils/seedDB.ts | 14 ++++++++------ api/src/utils/setProcessVariables.ts | 8 ++++---- 16 files changed, 41 insertions(+), 36 deletions(-) diff --git a/api/src/app-modules/configureCors.ts b/api/src/app-modules/configureCors.ts index 631b166..fc8a3ab 100644 --- a/api/src/app-modules/configureCors.ts +++ b/api/src/app-modules/configureCors.ts @@ -15,7 +15,7 @@ export const configureCors = (app: Express) => { whiteList.push(url.replace(/\/$/, '')) }) - console.log('All CORS Requests are enabled for:', whiteList) + process.logger.info('All CORS Requests are enabled for:', whiteList) app.use(cors({ credentials: true, origin: whiteList })) } } diff --git a/api/src/app-modules/configureLogger.ts b/api/src/app-modules/configureLogger.ts index 6edb3ba..a53f5f3 100644 --- a/api/src/app-modules/configureLogger.ts +++ b/api/src/app-modules/configureLogger.ts @@ -23,7 +23,7 @@ export const configureLogger = (app: Express) => { path: logsFolder }) - console.log('Writing Logs to :', path.join(logsFolder, filename)) + process.logger.info('Writing Logs to :', path.join(logsFolder, filename)) options = { stream: accessLogStream } } diff --git a/api/src/app.ts b/api/src/app.ts index 2c24ac4..f40da18 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -35,7 +35,7 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv) const app = express() const onError: ErrorRequestHandler = (err, req, res, next) => { - console.error(err.stack) + process.logger.error(err.stack) res.status(500).send('Something broke!') } @@ -76,7 +76,7 @@ export default setProcessVariables().then(async () => { const sasautosPath = path.join(process.driveLoc, 'sas', 'sasautos') if (await folderExists(sasautosPath)) { - console.log( + process.logger.warn( `SASAUTOS was not refreshed. To force a refresh, delete the ${sasautosPath} folder` ) } else { diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index edcc6f5..493952e 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -145,12 +145,12 @@ ${autoExecContent}` ]) .then(() => { session.completed = true - console.log('session completed', session) + process.logger.info('session completed', session) }) .catch((err) => { session.completed = true session.crashed = err.toString() - console.log('session crashed', session.id, session.crashed) + process.logger.error('session crashed', session.id, session.crashed) }) // we have a triggered session - add to array @@ -170,7 +170,10 @@ ${autoExecContent}` while ((await fileExists(codeFilePath)) && !session.crashed) {} if (session.crashed) - console.log('session crashed! while waiting to be ready', session.crashed) + process.logger.error( + 'session crashed! while waiting to be ready', + session.crashed + ) session.ready = true } diff --git a/api/src/controllers/internal/processProgram.ts b/api/src/controllers/internal/processProgram.ts index ff52c9d..dc7ff45 100644 --- a/api/src/controllers/internal/processProgram.ts +++ b/api/src/controllers/internal/processProgram.ts @@ -118,11 +118,11 @@ export const processProgram = async ( // copy the code file to log and end write stream writeStream.end(program) session.completed = true - console.log('session completed', session) + process.logger.info('session completed', session) } catch (err: any) { session.completed = true session.crashed = err.toString() - console.log('session crashed', session.id, session.crashed) + process.logger.error('session crashed', session.id, session.crashed) } } } diff --git a/api/src/controllers/mock-sas9.ts b/api/src/controllers/mock-sas9.ts index d4f7282..01a424f 100644 --- a/api/src/controllers/mock-sas9.ts +++ b/api/src/controllers/mock-sas9.ts @@ -107,7 +107,7 @@ export class MockSas9Controller { content: result.result as string } } catch (err) { - console.log('err', err) + process.logger.error('err', err) } return { @@ -168,7 +168,7 @@ export class MockSas9Controller { content: result.result as string } } catch (err) { - console.log('err', err) + process.logger.error('err', err) } return { @@ -269,7 +269,7 @@ const getMockResponseFromFile = async ( let file = await readFile(filePathParsed).catch((err: any) => { const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}` - console.error(errMsg) + process.logger.error(errMsg) error = true diff --git a/api/src/routes/appStream/index.ts b/api/src/routes/appStream/index.ts index 1c397d5..5227b10 100644 --- a/api/src/routes/appStream/index.ts +++ b/api/src/routes/appStream/index.ts @@ -58,7 +58,7 @@ export const publishAppStream = async ( ) const sasJsPort = process.env.PORT || 5000 - console.log( + process.logger.info( 'Serving Stream App: ', `http://localhost:${sasJsPort}/AppStream/${streamServiceName}` ) diff --git a/api/src/server.ts b/api/src/server.ts index 0743469..e859b87 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -7,11 +7,11 @@ appPromise.then(async (app) => { const protocol = process.env.PROTOCOL || 'http' const sasJsPort = process.env.PORT || 5000 - console.log('PROTOCOL: ', protocol) + process.logger.info('PROTOCOL: ', protocol) if (protocol !== 'https') { app.listen(sasJsPort, () => { - console.log( + process.logger.info( `⚡️[server]: Server is running at http://localhost:${sasJsPort}` ) }) @@ -20,7 +20,7 @@ appPromise.then(async (app) => { const httpsServer = createServer({ key, cert, ca }, app) httpsServer.listen(sasJsPort, () => { - console.log( + process.logger.info( `⚡️[server]: Server is running at https://localhost:${sasJsPort}` ) }) diff --git a/api/src/utils/appStreamConfig.ts b/api/src/utils/appStreamConfig.ts index f4f137d..672c679 100644 --- a/api/src/utils/appStreamConfig.ts +++ b/api/src/utils/appStreamConfig.ts @@ -36,7 +36,7 @@ export const loadAppStreamConfig = async () => { ) } - console.log('App Stream Config loaded!') + process.logger.info('App Stream Config loaded!') } export const addEntryToAppStreamConfig = ( diff --git a/api/src/utils/connectDB.ts b/api/src/utils/connectDB.ts index 9d47607..8849cd5 100644 --- a/api/src/utils/connectDB.ts +++ b/api/src/utils/connectDB.ts @@ -8,6 +8,6 @@ export const connectDB = async () => { throw new Error('Unable to connect to DB!') } - console.log('Connected to DB!') + process.logger.success('Connected to DB!') return seedDB() } diff --git a/api/src/utils/copySASjsCore.ts b/api/src/utils/copySASjsCore.ts index d45a72e..6f0e499 100644 --- a/api/src/utils/copySASjsCore.ts +++ b/api/src/utils/copySASjsCore.ts @@ -12,7 +12,7 @@ import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.' export const copySASjsCore = async () => { if (process.env.NODE_ENV === 'test') return - console.log('Copying Macros from container to drive.') + process.logger.log('Copying Macros from container to drive.') const macrosDrivePath = getMacrosFolder() @@ -30,5 +30,5 @@ export const copySASjsCore = async () => { await createFile(macroFileDestPath, macroContent) }) - console.log('Macros Drive Path:', macrosDrivePath) + process.logger.info('Macros Drive Path:', macrosDrivePath) } diff --git a/api/src/utils/createWeboutSasFile.ts b/api/src/utils/createWeboutSasFile.ts index 26f549f..ff29aa0 100644 --- a/api/src/utils/createWeboutSasFile.ts +++ b/api/src/utils/createWeboutSasFile.ts @@ -12,7 +12,7 @@ const fileContent = `%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta export const createWeboutSasFile = async () => { const macrosDrivePath = getMacrosFolder() - console.log(`Creating webout.sas at ${macrosDrivePath}`) + process.logger.log(`Creating webout.sas at ${macrosDrivePath}`) const filePath = path.join(macrosDrivePath, 'webout.sas') await createFile(filePath, fileContent) } diff --git a/api/src/utils/getCertificates.ts b/api/src/utils/getCertificates.ts index 6c295e0..ae6027c 100644 --- a/api/src/utils/getCertificates.ts +++ b/api/src/utils/getCertificates.ts @@ -10,9 +10,9 @@ export const getCertificates = async () => { const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)')) const caPath = CA_ROOT - console.log('keyPath: ', keyPath) - console.log('certPath: ', certPath) - if (caPath) console.log('caPath: ', caPath) + process.logger.info('keyPath: ', keyPath) + process.logger.info('certPath: ', certPath) + if (caPath) process.logger.info('caPath: ', caPath) const key = await readFile(keyPath) const cert = await readFile(certPath) diff --git a/api/src/utils/parseHelmetConfig.ts b/api/src/utils/parseHelmetConfig.ts index 4f52b37..de48589 100644 --- a/api/src/utils/parseHelmetConfig.ts +++ b/api/src/utils/parseHelmetConfig.ts @@ -22,12 +22,12 @@ export const getEnvCSPDirectives = ( try { cspConfigJson = JSON.parse(file) } catch (e) { - console.error( + process.logger.error( 'Parsing Content Security Policy JSON config failed. Make sure it is valid json' ) } } catch (e) { - console.error('Error reading HELMET CSP config file', e) + process.logger.error('Error reading HELMET CSP config file', e) } } diff --git a/api/src/utils/seedDB.ts b/api/src/utils/seedDB.ts index 5187900..1da77a6 100644 --- a/api/src/utils/seedDB.ts +++ b/api/src/utils/seedDB.ts @@ -19,7 +19,7 @@ export const seedDB = async (): Promise => { const client = new Client(CLIENT) await client.save() - console.log(`DB Seed - client created: ${CLIENT.clientId}`) + process.logger.success(`DB Seed - client created: ${CLIENT.clientId}`) } // Checking if 'AllUsers' Group is already in the database @@ -28,7 +28,7 @@ export const seedDB = async (): Promise => { const group = new Group(GROUP) groupExist = await group.save() - console.log(`DB Seed - Group created: ${GROUP.name}`) + process.logger.success(`DB Seed - Group created: ${GROUP.name}`) } // Checking if 'Public' Group is already in the database @@ -37,7 +37,7 @@ export const seedDB = async (): Promise => { const group = new Group(PUBLIC_GROUP) await group.save() - console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`) + process.logger.success(`DB Seed - Group created: ${PUBLIC_GROUP.name}`) } // Checking if user is already in the database @@ -46,12 +46,14 @@ export const seedDB = async (): Promise => { const user = new User(ADMIN_USER) usernameExist = await user.save() - console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`) + process.logger.success( + `DB Seed - admin account created: ${ADMIN_USER.username}` + ) } if (!groupExist.hasUser(usernameExist)) { groupExist.addUser(usernameExist) - console.log( + process.logger.success( `DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'` ) } @@ -62,7 +64,7 @@ export const seedDB = async (): Promise => { const configuration = new Configuration(SECRETS) configExist = await configuration.save() - console.log('DB Seed - configuration added') + process.logger.success('DB Seed - configuration added') } return { diff --git a/api/src/utils/setProcessVariables.ts b/api/src/utils/setProcessVariables.ts index e573998..c1a6cd4 100644 --- a/api/src/utils/setProcessVariables.ts +++ b/api/src/utils/setProcessVariables.ts @@ -62,8 +62,8 @@ export const setProcessVariables = async () => { process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784' - console.log('sasLoc: ', process.sasLoc) - console.log('sasDrive: ', process.driveLoc) - console.log('sasLogs: ', process.logsLoc) - console.log('runTimes: ', process.runTimes) + process.logger.info('sasLoc: ', process.sasLoc) + process.logger.info('sasDrive: ', process.driveLoc) + process.logger.info('sasLogs: ', process.logsLoc) + process.logger.info('runTimes: ', process.runTimes) } From b47e74a7e1afc71eaa9bb07ee4c2b46368004beb Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 22 Nov 2022 00:01:58 +0500 Subject: [PATCH 2/7] chore: styles fix --- web/src/containers/Studio/editor.tsx | 5 ++++- web/src/index.css | 12 ------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/web/src/containers/Studio/editor.tsx b/web/src/containers/Studio/editor.tsx index fa79d11..6a1b042 100644 --- a/web/src/containers/Studio/editor.tsx +++ b/web/src/containers/Studio/editor.tsx @@ -197,7 +197,10 @@ const SASjsEditor = ({

Log

-
+              
                 {log}
               
diff --git a/web/src/index.css b/web/src/index.css index e294eca..ebd047b 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -25,15 +25,3 @@ code { padding: '5px 10px'; margin-top: '10px'; } - -.tree-item-label { - display: flex; -} - -.tree-item-label.selected { - background: lightgoldenrodyellow; -} - -.tree-item-label:hover { - background: lightgray; -} From 4581f325344eb68c5df5a28492f132312f15bb5c Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 22 Nov 2022 00:02:59 +0500 Subject: [PATCH 3/7] feat(api): add the api endpoint for updating user password --- api/public/swagger.yaml | 34 +++++++++++++++++ api/src/controllers/auth.ts | 73 ++++++++++++++++++++++++++++++++++++- api/src/routes/api/auth.ts | 18 ++++++++- api/src/utils/validation.ts | 6 +++ 4 files changed, 129 insertions(+), 2 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 2935170..caab752 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -47,6 +47,21 @@ components: - userId type: object additionalProperties: false + UpdatePasswordPayload: + properties: + currentPassword: + type: string + description: 'Current Password' + example: currentPasswordString + newPassword: + type: string + description: 'New Password' + example: newPassword + required: + - currentPassword + - newPassword + type: object + additionalProperties: false ClientPayload: properties: clientId: @@ -632,6 +647,25 @@ paths: - bearerAuth: [] parameters: [] + /SASjsApi/auth/updatePassword: + patch: + operationId: UpdatePassword + responses: + '204': + description: 'No content' + summary: 'Update user''s password.' + tags: + - Auth + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdatePasswordPayload' /SASjsApi/authConfig: get: operationId: GetDetail diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index 78d5a5b..cddbd62 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -1,4 +1,16 @@ -import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa' +import express from 'express' +import { + Security, + Route, + Tags, + Example, + Post, + Patch, + Request, + Body, + Query, + Hidden +} from 'tsoa' import jwt from 'jsonwebtoken' import { InfoJWT } from '../types' import { @@ -9,6 +21,7 @@ import { saveTokensInDB } from '../utils' import Client from '../model/Client' +import User from '../model/User' @Route('SASjsApi/auth') @Tags('Auth') @@ -62,6 +75,18 @@ export class AuthController { public async logout(@Query() @Hidden() data?: InfoJWT) { return logout(data!) } + + /** + * @summary Update user's password. + */ + @Security('bearerAuth') + @Patch('updatePassword') + public async updatePassword( + @Request() req: express.Request, + @Body() body: UpdatePasswordPayload + ) { + return updatePassword(req, body) + } } const token = async (data: any): Promise => { @@ -128,6 +153,39 @@ const logout = async (userInfo: InfoJWT) => { await removeTokensInDB(userInfo.userId, userInfo.clientId) } +const updatePassword = async ( + req: express.Request, + data: UpdatePasswordPayload +) => { + const { currentPassword, newPassword } = data + const userId = req.user?.userId + const dbUser = await User.findOne({ userId }) + + if (!dbUser) + throw { + code: 404, + message: `User not found!` + } + + if (dbUser?.authProvider) { + throw { + code: 405, + message: + 'Can not update password of user that is created by an external auth provider.' + } + } + + const validPass = dbUser.comparePassword(currentPassword) + if (!validPass) + throw { + code: 403, + message: `Invalid current password!` + } + + dbUser.password = User.hashPassword(newPassword) + await dbUser.save() +} + interface TokenPayload { /** * Client ID @@ -154,6 +212,19 @@ interface TokenResponse { refreshToken: string } +interface UpdatePasswordPayload { + /** + * Current Password + * @example "currentPasswordString" + */ + currentPassword: string + /** + * New Password + * @example "newPassword" + */ + newPassword: string +} + const verifyAuthCode = async ( clientId: string, code: string diff --git a/api/src/routes/api/auth.ts b/api/src/routes/api/auth.ts index f031df9..3e8a9ac 100644 --- a/api/src/routes/api/auth.ts +++ b/api/src/routes/api/auth.ts @@ -7,12 +7,28 @@ import { authenticateRefreshToken } from '../../middlewares' -import { tokenValidation } from '../../utils' +import { tokenValidation, updatePasswordValidation } from '../../utils' import { InfoJWT } from '../../types' const authRouter = express.Router() const controller = new AuthController() +authRouter.patch( + '/updatePassword', + authenticateAccessToken, + async (req, res) => { + const { error, value: body } = updatePasswordValidation(req.body) + if (error) return res.status(400).send(error.details[0].message) + + try { + await controller.updatePassword(req, body) + res.sendStatus(204) + } catch (err: any) { + res.status(err.code).send(err.message) + } + } +) + authRouter.post('/token', async (req, res) => { const { error, value: body } = tokenValidation(req.body) if (error) return res.status(400).send(error.details[0].message) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 6b1e067..feed970 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -85,6 +85,12 @@ export const updateUserValidation = ( return Joi.object(validationChecks).validate(data) } +export const updatePasswordValidation = (data: any): Joi.ValidationResult => + Joi.object({ + currentPassword: Joi.string().required(), + newPassword: passwordSchema.required() + }).validate(data) + export const registerClientValidation = (data: any): Joi.ValidationResult => Joi.object({ clientId: Joi.string().required(), From 8b8c43c21bde5379825c5ec44ecd81a92425f605 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 22 Nov 2022 00:03:25 +0500 Subject: [PATCH 4/7] feat(web): add the UI for updating user password --- web/src/components/passwordModal.tsx | 141 ++++++++++++++++++ web/src/containers/Settings/profile.tsx | 182 +++++++++++++++--------- 2 files changed, 254 insertions(+), 69 deletions(-) create mode 100644 web/src/components/passwordModal.tsx diff --git a/web/src/components/passwordModal.tsx b/web/src/components/passwordModal.tsx new file mode 100644 index 0000000..4482367 --- /dev/null +++ b/web/src/components/passwordModal.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useState } from 'react' + +import { + Grid, + DialogContent, + DialogActions, + Button, + OutlinedInput, + InputAdornment, + IconButton, + FormControl, + InputLabel, + FormHelperText +} from '@mui/material' +import Visibility from '@mui/icons-material/Visibility' +import VisibilityOff from '@mui/icons-material/VisibilityOff' + +import { BootstrapDialogTitle } from './dialogTitle' +import { BootstrapDialog } from './modal' + +type Props = { + open: boolean + setOpen: React.Dispatch> + title: string + updatePassword: (currentPassword: string, newPassword: string) => void +} + +const UpdatePasswordModal = (props: Props) => { + const { open, setOpen, title, updatePassword } = props + const [currentPassword, setCurrentPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [hasError, setHasError] = useState(false) + const [errorText, setErrorText] = useState('') + + useEffect(() => { + if (newPassword.length >= 6) { + setErrorText('') + setHasError(false) + } + }, [newPassword]) + + const handleBlur = () => { + if (newPassword.length < 6) { + setErrorText('Password length should be at least 6 characters.') + setHasError(true) + } else { + setErrorText('') + setHasError(false) + } + } + + return ( +
+ setOpen(false)} open={open}> + + {title} + + + + + + + + + + + + + + + + +
+ ) +} + +export default UpdatePasswordModal + +type PasswordInputProps = { + label: string + password: string + setPassword: React.Dispatch> + hasError?: boolean + errorText?: string + handleBlur?: () => void +} + +const PasswordInput = ({ + label, + password, + setPassword, + hasError, + errorText, + handleBlur +}: PasswordInputProps) => { + const [showPassword, setShowPassword] = useState(false) + + return ( + + {label} + setPassword(e.target.value)} + onBlur={handleBlur} + endAdornment={ + + setShowPassword((val) => !val)} + edge="end" + > + {showPassword ? : } + + + } + /> + {errorText && {errorText}} + + ) +} diff --git a/web/src/containers/Settings/profile.tsx b/web/src/containers/Settings/profile.tsx index 06602e7..5f36610 100644 --- a/web/src/containers/Settings/profile.tsx +++ b/web/src/containers/Settings/profile.tsx @@ -17,11 +17,13 @@ import { import { toast } from 'react-toastify' import { AppContext, ModeType } from '../../context/appContext' +import UpdatePasswordModal from '../../components/passwordModal' const Profile = () => { const [isLoading, setIsLoading] = useState(false) const appContext = useContext(AppContext) const [user, setUser] = useState({} as any) + const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false) useEffect(() => { setIsLoading(true) @@ -36,7 +38,7 @@ const Profile = () => { .finally(() => { setIsLoading(false) }) - }, []) + }, [appContext.userId]) const handleChange = (event: any) => { const { name, value } = event.target @@ -68,82 +70,124 @@ const Profile = () => { }) } + const updatePassword = (currentPassword: string, newPassword: string) => { + setIsLoading(true) + setIsPasswordModalOpen(false) + axios + .patch(`/SASjsApi/auth/updatePassword`, { + currentPassword, + newPassword + }) + .then((res: any) => { + 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 ? ( ) : ( - - - - - - - - - - - - - - - - - - - - } - label="isActive" + <> + + + + + + + - } - label="isAdmin" + + + + - + + + + + + + + + } + label="isActive" + /> + } + label="isAdmin" + /> + + + + + + - - - - - - - + + + + + + + + ) } From 68758aa6165e1872b2264d683e7b419be96d40fa Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 22 Nov 2022 15:26:22 +0500 Subject: [PATCH 5/7] chore: new password should be different to current password --- web/src/components/passwordModal.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/web/src/components/passwordModal.tsx b/web/src/components/passwordModal.tsx index 4482367..f9c1ee8 100644 --- a/web/src/components/passwordModal.tsx +++ b/web/src/components/passwordModal.tsx @@ -33,19 +33,23 @@ const UpdatePasswordModal = (props: Props) => { const [errorText, setErrorText] = useState('') useEffect(() => { - if (newPassword.length >= 6) { + 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) } - }, [newPassword]) + }, [currentPassword, newPassword]) const handleBlur = () => { if (newPassword.length < 6) { setErrorText('Password length should be at least 6 characters.') setHasError(true) - } else { - setErrorText('') - setHasError(false) } } From 1d48f8856b1fbbf3ef868914558333190e04981f Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 22 Nov 2022 19:58:17 +0500 Subject: [PATCH 6/7] feat: ask for updated password on first login --- api/public/swagger.yaml | 25 +++++- api/src/controllers/auth.ts | 1 + api/src/controllers/authConfig.ts | 3 +- api/src/controllers/session.ts | 9 +- api/src/controllers/web.ts | 6 +- api/src/middlewares/authenticateToken.ts | 3 +- api/src/middlewares/desktop.ts | 3 +- api/src/model/User.ts | 5 ++ api/src/types/RequestUser.ts | 1 + api/src/utils/isPublicRoute.ts | 3 +- api/src/utils/verifyTokenInDB.ts | 2 + web/src/App.tsx | 15 ++++ web/src/components/login.tsx | 1 + web/src/components/passwordModal.tsx | 2 +- web/src/components/updatePassword.tsx | 109 +++++++++++++++++++++++ web/src/context/appContext.tsx | 8 ++ 16 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 web/src/components/updatePassword.tsx 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, From c26485afec8d285b22d4312418989b270e41bc35 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 22 Nov 2022 20:15:26 +0500 Subject: [PATCH 7/7] chore: fix specs --- api/src/routes/api/spec/web.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/routes/api/spec/web.spec.ts b/api/src/routes/api/spec/web.spec.ts index eebee67..c5ce0ca 100644 --- a/api/src/routes/api/spec/web.spec.ts +++ b/api/src/routes/api/spec/web.spec.ts @@ -77,7 +77,8 @@ describe('web', () => { id: expect.any(Number), username: user.username, displayName: user.displayName, - isAdmin: user.isAdmin + isAdmin: user.isAdmin, + needsToUpdatePassword: true }) })