From 2413c05fea3960f7e5c3c8b7b2f85d61314f08db Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 10 Nov 2022 19:43:06 +0500 Subject: [PATCH 1/5] feat: make access token duration configurable when creating client/secret --- api/public/swagger.yaml | 9 +++++++-- api/src/controllers/auth.ts | 17 +++++++++++++++-- api/src/controllers/client.ts | 15 +++++++++------ api/src/model/Client.ts | 9 +++++++++ api/src/utils/generateAccessToken.ts | 4 ++-- api/src/utils/validation.ts | 3 ++- 6 files changed, 44 insertions(+), 13 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 9d97d3b..5d6d695 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -57,6 +57,11 @@ components: type: string description: 'Client Secret' example: someRandomCryptoString + accessTokenExpiryDays: + type: number + format: double + description: 'Number of days in which access token will expire' + example: 1 required: - clientId - clientSecret @@ -679,8 +684,8 @@ paths: $ref: '#/components/schemas/ClientPayload' examples: 'Example 1': - value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString} - summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.' + value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiryDays: 1} + summary: 'Create client with the following attributes: ClientId, ClientSecret, accessTokenExpires (optional) . Admin only task.' tags: - Client security: diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index 737468e..138f237 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -8,6 +8,7 @@ import { removeTokensInDB, saveTokensInDB } from '../utils' +import Client from '../model/Client' @Route('SASjsApi/auth') @Tags('Auth') @@ -83,7 +84,13 @@ const token = async (data: any): Promise => { } } - const accessToken = generateAccessToken(userInfo) + const client = await Client.findOne({ clientId }) + if (!client) throw new Error('Invalid clientId.') + + const accessToken = generateAccessToken( + userInfo, + client.accessTokenExpiryDays + ) const refreshToken = generateRefreshToken(userInfo) await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken) @@ -92,7 +99,13 @@ const token = async (data: any): Promise => { } const refresh = async (userInfo: InfoJWT): Promise => { - const accessToken = generateAccessToken(userInfo) + const client = await Client.findOne({ clientId: userInfo.clientId }) + if (!client) throw new Error('Invalid clientId.') + + const accessToken = generateAccessToken( + userInfo, + client.accessTokenExpiryDays + ) const refreshToken = generateRefreshToken(userInfo) await saveTokensInDB( diff --git a/api/src/controllers/client.ts b/api/src/controllers/client.ts index c65f52a..c0b32a5 100644 --- a/api/src/controllers/client.ts +++ b/api/src/controllers/client.ts @@ -7,12 +7,13 @@ import Client, { ClientPayload } from '../model/Client' @Tags('Client') export class ClientController { /** - * @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task. + * @summary Create client with the following attributes: ClientId, ClientSecret, accessTokenExpires (optional) . Admin only task. * */ @Example({ clientId: 'someFormattedClientID1234', - clientSecret: 'someRandomCryptoString' + clientSecret: 'someRandomCryptoString', + accessTokenExpiryDays: 1 }) @Post('/') public async createClient( @@ -22,8 +23,8 @@ export class ClientController { } } -const createClient = async (data: any): Promise => { - const { clientId, clientSecret } = data +const createClient = async (data: ClientPayload): Promise => { + const { clientId, clientSecret, accessTokenExpiryDays } = data // Checking if client is already in the database const clientExist = await Client.findOne({ clientId }) @@ -32,13 +33,15 @@ const createClient = async (data: any): Promise => { // Create a new client const client = new Client({ clientId, - clientSecret + clientSecret, + accessTokenExpiryDays }) const savedClient = await client.save() return { clientId: savedClient.clientId, - clientSecret: savedClient.clientSecret + clientSecret: savedClient.clientSecret, + accessTokenExpiryDays: savedClient.accessTokenExpiryDays } } diff --git a/api/src/model/Client.ts b/api/src/model/Client.ts index 113bffa..2f9da19 100644 --- a/api/src/model/Client.ts +++ b/api/src/model/Client.ts @@ -11,6 +11,11 @@ export interface ClientPayload { * @example "someRandomCryptoString" */ clientSecret: string + /** + * Number of days in which access token will expire + * @example 1 + */ + accessTokenExpiryDays?: number } const ClientSchema = new Schema({ @@ -21,6 +26,10 @@ const ClientSchema = new Schema({ clientSecret: { type: String, required: true + }, + accessTokenExpiryDays: { + type: Number, + default: 1 } }) diff --git a/api/src/utils/generateAccessToken.ts b/api/src/utils/generateAccessToken.ts index 2b385b6..ec25c61 100644 --- a/api/src/utils/generateAccessToken.ts +++ b/api/src/utils/generateAccessToken.ts @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken' import { InfoJWT } from '../types' -export const generateAccessToken = (data: InfoJWT) => +export const generateAccessToken = (data: InfoJWT, expiry?: number) => jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, { - expiresIn: '1day' + expiresIn: expiry ? `${expiry}d` : '1d' }) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index e3a3d3d..7d38dce 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -88,7 +88,8 @@ export const updateUserValidation = ( export const registerClientValidation = (data: any): Joi.ValidationResult => Joi.object({ clientId: Joi.string().required(), - clientSecret: Joi.string().required() + clientSecret: Joi.string().required(), + accessTokenExpiryDays: Joi.number() }).validate(data) export const registerPermissionValidation = (data: any): Joi.ValidationResult => From abd5c64b4a726e3f17594a98111b6aa269b71fee Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 10 Nov 2022 21:02:20 +0500 Subject: [PATCH 2/5] feat: make refresh token duration configurable --- api/public/swagger.yaml | 7 ++++++- api/src/controllers/auth.ts | 10 ++++++++-- api/src/controllers/client.ts | 13 ++++++++++--- api/src/model/Client.ts | 9 +++++++++ api/src/utils/generateRefreshToken.ts | 4 ++-- api/src/utils/validation.ts | 3 ++- 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 5d6d695..7b2c7ce 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -62,6 +62,11 @@ components: format: double description: 'Number of days in which access token will expire' example: 1 + refreshTokenExpiryDays: + type: number + format: double + description: 'Number of days in which access token will expire' + example: 30 required: - clientId - clientSecret @@ -684,7 +689,7 @@ paths: $ref: '#/components/schemas/ClientPayload' examples: 'Example 1': - value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiryDays: 1} + value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiryDays: 1, refreshTokenExpiryDays: 30} summary: 'Create client with the following attributes: ClientId, ClientSecret, accessTokenExpires (optional) . Admin only task.' tags: - Client diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index 138f237..2b94b8a 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -91,7 +91,10 @@ const token = async (data: any): Promise => { userInfo, client.accessTokenExpiryDays ) - const refreshToken = generateRefreshToken(userInfo) + const refreshToken = generateRefreshToken( + userInfo, + client.refreshTokenExpiryDays + ) await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken) @@ -106,7 +109,10 @@ const refresh = async (userInfo: InfoJWT): Promise => { userInfo, client.accessTokenExpiryDays ) - const refreshToken = generateRefreshToken(userInfo) + const refreshToken = generateRefreshToken( + userInfo, + client.refreshTokenExpiryDays + ) await saveTokensInDB( userInfo.userId, diff --git a/api/src/controllers/client.ts b/api/src/controllers/client.ts index c0b32a5..1f15dfa 100644 --- a/api/src/controllers/client.ts +++ b/api/src/controllers/client.ts @@ -13,7 +13,8 @@ export class ClientController { @Example({ clientId: 'someFormattedClientID1234', clientSecret: 'someRandomCryptoString', - accessTokenExpiryDays: 1 + accessTokenExpiryDays: 1, + refreshTokenExpiryDays: 30 }) @Post('/') public async createClient( @@ -24,7 +25,12 @@ export class ClientController { } const createClient = async (data: ClientPayload): Promise => { - const { clientId, clientSecret, accessTokenExpiryDays } = data + const { + clientId, + clientSecret, + accessTokenExpiryDays, + refreshTokenExpiryDays + } = data // Checking if client is already in the database const clientExist = await Client.findOne({ clientId }) @@ -42,6 +48,7 @@ const createClient = async (data: ClientPayload): Promise => { return { clientId: savedClient.clientId, clientSecret: savedClient.clientSecret, - accessTokenExpiryDays: savedClient.accessTokenExpiryDays + accessTokenExpiryDays: savedClient.accessTokenExpiryDays, + refreshTokenExpiryDays: savedClient.refreshTokenExpiryDays } } diff --git a/api/src/model/Client.ts b/api/src/model/Client.ts index 2f9da19..1fcb204 100644 --- a/api/src/model/Client.ts +++ b/api/src/model/Client.ts @@ -16,6 +16,11 @@ export interface ClientPayload { * @example 1 */ accessTokenExpiryDays?: number + /** + * Number of days in which access token will expire + * @example 30 + */ + refreshTokenExpiryDays?: number } const ClientSchema = new Schema({ @@ -30,6 +35,10 @@ const ClientSchema = new Schema({ accessTokenExpiryDays: { type: Number, default: 1 + }, + refreshTokenExpiryDays: { + type: Number, + default: 30 } }) diff --git a/api/src/utils/generateRefreshToken.ts b/api/src/utils/generateRefreshToken.ts index a8365ff..03f7bc1 100644 --- a/api/src/utils/generateRefreshToken.ts +++ b/api/src/utils/generateRefreshToken.ts @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken' import { InfoJWT } from '../types' -export const generateRefreshToken = (data: InfoJWT) => +export const generateRefreshToken = (data: InfoJWT, expiry?: number) => jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, { - expiresIn: '30 days' + expiresIn: expiry ? `${expiry}d` : '30d' }) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 7d38dce..9ffcdb6 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -89,7 +89,8 @@ export const registerClientValidation = (data: any): Joi.ValidationResult => Joi.object({ clientId: Joi.string().required(), clientSecret: Joi.string().required(), - accessTokenExpiryDays: Joi.number() + accessTokenExpiryDays: Joi.number(), + refreshTokenExpiryDays: Joi.number() }).validate(data) export const registerPermissionValidation = (data: any): Joi.ValidationResult => From 4ca61feda681d4d2beabeac351dc163cc16df71f Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 10 Nov 2022 21:05:41 +0500 Subject: [PATCH 3/5] chore: npm audit fix --- api/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 96a6fb2..115531f 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -7092,9 +7092,9 @@ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -15592,9 +15592,9 @@ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } From acc25cbd686952d3f1c65e57aefcebe1cb859cc7 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 11 Nov 2022 15:27:12 +0500 Subject: [PATCH 4/5] fix(web): dispose monaco editor actions in return of useEffect --- .../containers/Studio/internal/hooks/useEditor.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/web/src/containers/Studio/internal/hooks/useEditor.ts b/web/src/containers/Studio/internal/hooks/useEditor.ts index 48fdb61..6dcfe7e 100644 --- a/web/src/containers/Studio/internal/hooks/useEditor.ts +++ b/web/src/containers/Studio/internal/hooks/useEditor.ts @@ -49,7 +49,7 @@ const useEditor = ({ const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false) const [showDiff, setShowDiff] = useState(false) - const editorRef = useRef(null as any) + const editorRef = useRef(null) const handleEditorDidMount: EditorDidMount = (editor) => { editorRef.current = editor @@ -199,7 +199,7 @@ const useEditor = ({ } useEffect(() => { - editorRef.current.addAction({ + const saveFileAction = editorRef.current?.addAction({ // An unique identifier of the contributed action. id: 'save-file', @@ -209,6 +209,8 @@ const useEditor = ({ // An optional array of keybindings for the action. keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], + contextMenuGroupId: '9_cutcopypaste', + // Method that will be executed when the action is triggered. // @param editor The editor instance is passed in as a convenience run: () => { @@ -217,7 +219,7 @@ const useEditor = ({ } }) - editorRef.current.addAction({ + const runCodeAction = editorRef.current?.addAction({ // An unique identifier of the contributed action. id: 'run-code', @@ -229,14 +231,17 @@ const useEditor = ({ contextMenuGroupId: 'navigation', - contextMenuOrder: 1, - // Method that will be executed when the action is triggered. // @param editor The editor instance is passed in as a convenience run: function () { runCode(getSelection(editorRef.current as any) || fileContent) } }) + + return () => { + saveFileAction?.dispose() + runCodeAction?.dispose() + } }, [fileContent, prevFileContent, selectedFilePath, saveFile, runCode]) useEffect(() => { From fe07c41f5f5b568b1137aaea627687d06986d776 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 11 Nov 2022 15:35:24 +0500 Subject: [PATCH 5/5] chore: update header --- api/public/swagger.yaml | 2 +- api/src/controllers/client.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 7b2c7ce..6c049cd 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -690,7 +690,7 @@ paths: examples: 'Example 1': value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiryDays: 1, refreshTokenExpiryDays: 30} - summary: 'Create client with the following attributes: ClientId, ClientSecret, accessTokenExpires (optional) . Admin only task.' + summary: "Admin only task. Create client with the following attributes:\nClientId,\nClientSecret,\naccessTokenExpiryDays (optional),\nrefreshTokenExpiryDays (optional)" tags: - Client security: diff --git a/api/src/controllers/client.ts b/api/src/controllers/client.ts index 1f15dfa..f4e7122 100644 --- a/api/src/controllers/client.ts +++ b/api/src/controllers/client.ts @@ -7,7 +7,11 @@ import Client, { ClientPayload } from '../model/Client' @Tags('Client') export class ClientController { /** - * @summary Create client with the following attributes: ClientId, ClientSecret, accessTokenExpires (optional) . Admin only task. + * @summary Admin only task. Create client with the following attributes: + * ClientId, + * ClientSecret, + * accessTokenExpiryDays (optional), + * refreshTokenExpiryDays (optional) * */ @Example({