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" } diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 9d97d3b..6c049cd 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -57,6 +57,16 @@ 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 + refreshTokenExpiryDays: + type: number + format: double + description: 'Number of days in which access token will expire' + example: 30 required: - clientId - clientSecret @@ -679,8 +689,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, refreshTokenExpiryDays: 30} + 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/auth.ts b/api/src/controllers/auth.ts index 737468e..2b94b8a 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,8 +84,17 @@ const token = async (data: any): Promise => { } } - const accessToken = generateAccessToken(userInfo) - const refreshToken = generateRefreshToken(userInfo) + const client = await Client.findOne({ clientId }) + if (!client) throw new Error('Invalid clientId.') + + const accessToken = generateAccessToken( + userInfo, + client.accessTokenExpiryDays + ) + const refreshToken = generateRefreshToken( + userInfo, + client.refreshTokenExpiryDays + ) await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken) @@ -92,8 +102,17 @@ const token = async (data: any): Promise => { } const refresh = async (userInfo: InfoJWT): Promise => { - const accessToken = generateAccessToken(userInfo) - const refreshToken = generateRefreshToken(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, + client.refreshTokenExpiryDays + ) await saveTokensInDB( userInfo.userId, diff --git a/api/src/controllers/client.ts b/api/src/controllers/client.ts index c65f52a..f4e7122 100644 --- a/api/src/controllers/client.ts +++ b/api/src/controllers/client.ts @@ -7,12 +7,18 @@ import Client, { ClientPayload } from '../model/Client' @Tags('Client') export class ClientController { /** - * @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task. + * @summary Admin only task. Create client with the following attributes: + * ClientId, + * ClientSecret, + * accessTokenExpiryDays (optional), + * refreshTokenExpiryDays (optional) * */ @Example({ clientId: 'someFormattedClientID1234', - clientSecret: 'someRandomCryptoString' + clientSecret: 'someRandomCryptoString', + accessTokenExpiryDays: 1, + refreshTokenExpiryDays: 30 }) @Post('/') public async createClient( @@ -22,8 +28,13 @@ export class ClientController { } } -const createClient = async (data: any): Promise => { - const { clientId, clientSecret } = data +const createClient = async (data: ClientPayload): Promise => { + const { + clientId, + clientSecret, + accessTokenExpiryDays, + refreshTokenExpiryDays + } = data // Checking if client is already in the database const clientExist = await Client.findOne({ clientId }) @@ -32,13 +43,16 @@ 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, + refreshTokenExpiryDays: savedClient.refreshTokenExpiryDays } } diff --git a/api/src/model/Client.ts b/api/src/model/Client.ts index 113bffa..1fcb204 100644 --- a/api/src/model/Client.ts +++ b/api/src/model/Client.ts @@ -11,6 +11,16 @@ export interface ClientPayload { * @example "someRandomCryptoString" */ clientSecret: string + /** + * Number of days in which access token will expire + * @example 1 + */ + accessTokenExpiryDays?: number + /** + * Number of days in which access token will expire + * @example 30 + */ + refreshTokenExpiryDays?: number } const ClientSchema = new Schema({ @@ -21,6 +31,14 @@ const ClientSchema = new Schema({ clientSecret: { type: String, required: true + }, + accessTokenExpiryDays: { + type: Number, + default: 1 + }, + refreshTokenExpiryDays: { + type: Number, + default: 30 } }) 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/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 e3a3d3d..9ffcdb6 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -88,7 +88,9 @@ 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(), + refreshTokenExpiryDays: Joi.number() }).validate(data) export const registerPermissionValidation = (data: any): Joi.ValidationResult => 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(() => {