diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index ed3b794..923115f 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -5,51 +5,6 @@ components: requestBodies: {} responses: {} schemas: - LoginPayload: - properties: - username: - type: string - description: 'Username for user' - example: secretuser - password: - type: string - description: 'Password for user' - example: secretpassword - required: - - username - - password - type: object - additionalProperties: false - AuthorizeResponse: - properties: - code: - type: string - description: 'Authorization code' - example: someRandomCryptoString - required: - - code - type: object - additionalProperties: false - AuthorizePayload: - properties: - username: - type: string - description: 'Username for user' - example: secretuser - password: - type: string - description: 'Password for user' - example: secretpassword - clientId: - type: string - description: 'Client ID' - example: clientID1 - required: - - username - - password - - clientId - type: object - additionalProperties: false TokenResponse: properties: accessToken: @@ -92,6 +47,41 @@ components: - userId type: object additionalProperties: false + LoginPayload: + properties: + username: + type: string + description: 'Username for user' + example: secretuser + password: + type: string + description: 'Password for user' + example: secretpassword + required: + - username + - password + type: object + additionalProperties: false + AuthorizeResponse: + properties: + code: + type: string + description: 'Authorization code' + example: someRandomCryptoString + required: + - code + type: object + additionalProperties: false + AuthorizePayload: + properties: + clientId: + type: string + description: 'Client ID' + example: clientID1 + required: + - clientId + type: object + additionalProperties: false ClientPayload: properties: clientId: @@ -425,14 +415,6 @@ components: - description type: object additionalProperties: false - ExecuteReturnJsonPayload: - properties: - _program: - type: string - description: 'Location of SAS program' - example: /Public/somefolder/some.file - type: object - additionalProperties: false InfoResponse: properties: mode: @@ -452,6 +434,14 @@ components: - protocol type: object additionalProperties: false + ExecuteReturnJsonPayload: + properties: + _program: + type: string + description: 'Location of SAS program' + example: /Public/somefolder/some.file + type: object + additionalProperties: false securitySchemes: bearerAuth: type: http @@ -465,86 +455,6 @@ info: name: '4GL Ltd' openapi: 3.0.0 paths: - /: - get: - operationId: Home - responses: - '200': - description: Ok - content: - application/json: - schema: - type: string - summary: 'Render index.html' - tags: - - Web - security: [] - parameters: [] - /login: - post: - operationId: Login - responses: - '200': - description: Ok - content: - application/json: - schema: - properties: - user: {properties: {displayName: {type: string}, username: {type: string}}, required: [displayName, username], type: object} - loggedIn: {type: boolean} - required: - - user - - loggedIn - type: object - summary: 'Accept a valid username/password' - tags: - - Web - security: [] - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/LoginPayload' - /logout: - get: - operationId: Logout - responses: - '200': - description: Ok - content: - application/json: - schema: {} - summary: 'Accept a valid username/password' - tags: - - Web - security: [] - parameters: [] - /SASjsApi/auth/authorize: - post: - operationId: Authorize - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/AuthorizeResponse' - examples: - 'Example 1': - value: {code: someRandomCryptoString} - summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE' - tags: - - Auth - security: [] - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AuthorizePayload' /SASjsApi/auth/token: post: operationId: Token @@ -602,6 +512,86 @@ paths: - bearerAuth: [] parameters: [] + /: + get: + operationId: Home + responses: + '200': + description: Ok + content: + application/json: + schema: + type: string + summary: 'Render index.html' + tags: + - Web + security: [] + parameters: [] + /SASLogon/login: + post: + operationId: Login + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + user: {properties: {displayName: {type: string}, username: {type: string}}, required: [displayName, username], type: object} + loggedIn: {type: boolean} + required: + - user + - loggedIn + type: object + summary: 'Accept a valid username/password' + tags: + - Web + security: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginPayload' + /SASLogon/authorize: + post: + operationId: Authorize + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizeResponse' + examples: + 'Example 1': + value: {code: someRandomCryptoString} + summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE' + tags: + - Web + security: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizePayload' + /logout: + get: + operationId: Logout + responses: + '200': + description: Ok + content: + application/json: + schema: {} + summary: 'Accept a valid username/password' + tags: + - Web + security: [] + parameters: [] /SASjsApi/client: post: operationId: CreateClient @@ -1248,6 +1238,24 @@ paths: format: double type: number example: '6789' + /SASjsApi/info: + get: + operationId: Info + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/InfoResponse' + examples: + 'Example 1': + value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http} + summary: 'Get server info (mode, cors, whiteList, protocol).' + tags: + - Info + security: [] + parameters: [] /SASjsApi/session: get: operationId: Session @@ -1330,24 +1338,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ExecuteReturnJsonPayload' - /SASjsApi/info: - get: - operationId: Info - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/InfoResponse' - examples: - 'Example 1': - value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http} - summary: 'Get server info (mode, cors, whiteList, protocol).' - tags: - - Info - security: [] - parameters: [] servers: - url: / diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index dba3db9..e642213 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -1,11 +1,8 @@ import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa' import jwt from 'jsonwebtoken' -import User from '../model/User' -import Client from '../model/Client' import { InfoJWT } from '../types' import { generateAccessToken, - generateAuthCode, generateRefreshToken, removeTokensInDB, saveTokensInDB @@ -25,20 +22,6 @@ export class AuthController { static deleteCode = (userId: number, clientId: string) => delete AuthController.authCodes[userId][clientId] - /** - * @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE - * - */ - @Example({ - code: 'someRandomCryptoString' - }) - @Post('/authorize') - public async authorize( - @Body() body: AuthorizePayload - ): Promise { - return authorize(body) - } - /** * @summary Accepts client/auth code and returns access/refresh tokens * @@ -79,33 +62,6 @@ export class AuthController { } } -const authorize = async (data: any): Promise => { - const { username, password, clientId } = data - - const client = await Client.findOne({ clientId }) - if (!client) throw new Error('Invalid clientId.') - - // Authenticate User - const user = await User.findOne({ username }) - if (!user) throw new Error('Username is not found.') - - const validPass = user.comparePassword(password) - if (!validPass) throw new Error('Invalid password.') - - // generate authorization code against clientId - const userInfo: InfoJWT = { - clientId, - userId: user.id - } - const code = AuthController.saveCode( - user.id, - clientId, - generateAuthCode(userInfo) - ) - - return { code } -} - const token = async (data: any): Promise => { const { clientId, code } = data @@ -143,32 +99,6 @@ const logout = async (userInfo: InfoJWT) => { await removeTokensInDB(userInfo.userId, userInfo.clientId) } -interface AuthorizePayload { - /** - * Username for user - * @example "secretuser" - */ - username: string - /** - * Password for user - * @example "secretpassword" - */ - password: string - /** - * Client ID - * @example "clientID1" - */ - clientId: string -} - -interface AuthorizeResponse { - /** - * Authorization code - * @example "someRandomCryptoString" - */ - code: string -} - interface TokenPayload { /** * Client ID diff --git a/api/src/controllers/index.ts b/api/src/controllers/index.ts index 05e8b84..80bbbd1 100644 --- a/api/src/controllers/index.ts +++ b/api/src/controllers/index.ts @@ -3,7 +3,8 @@ export * from './client' export * from './code' export * from './drive' export * from './group' +export * from './info' export * from './session' export * from './stp' export * from './user' -export * from './info' +export * from './web' diff --git a/api/src/controllers/web.ts b/api/src/controllers/web.ts index 7826f57..e82f192 100644 --- a/api/src/controllers/web.ts +++ b/api/src/controllers/web.ts @@ -1,10 +1,13 @@ import path from 'path' import express from 'express' -import { Request, Route, Tags, Post, Body, Get } from 'tsoa' +import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa' import { readFile } from '@sasjs/utils' import User from '../model/User' -import { getWebBuildFolderPath } from '../utils' +import Client from '../model/Client' +import { getWebBuildFolderPath, generateAuthCode } from '../utils' +import { InfoJWT } from '../types' +import { AuthController } from './auth' @Route('/') @Tags('Web') @@ -22,7 +25,7 @@ export class WebController { * @summary Accept a valid username/password * */ - @Post('/login') + @Post('/SASLogon/login') public async login( @Request() req: express.Request, @Body() body: LoginPayload @@ -30,6 +33,21 @@ export class WebController { return login(req, body) } + /** + * @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE + * + */ + @Example({ + code: 'someRandomCryptoString' + }) + @Post('/SASLogon/authorize') + public async authorize( + @Request() req: express.Request, + @Body() body: AuthorizePayload + ): Promise { + return authorize(req, body.clientId) + } + /** * @summary Accept a valid username/password * @@ -84,6 +102,26 @@ const login = async ( } } +const authorize = async ( + req: express.Request, + clientId: string +): Promise => { + const userId = req.session.user?.userId + if (!userId) throw new Error('Invalid userId.') + // generate authorization code against clientId + const userInfo: InfoJWT = { + clientId, + userId + } + const code = AuthController.saveCode( + userId, + clientId, + generateAuthCode(userInfo) + ) + + return { code } +} + interface LoginPayload { /** * Username for user @@ -96,3 +134,19 @@ interface LoginPayload { */ password: string } + +interface AuthorizePayload { + /** + * Client ID + * @example "clientID1" + */ + clientId: string +} + +interface AuthorizeResponse { + /** + * Authorization code + * @example "someRandomCryptoString" + */ + code: string +} diff --git a/api/src/routes/api/auth.ts b/api/src/routes/api/auth.ts index 8965052..d463b22 100644 --- a/api/src/routes/api/auth.ts +++ b/api/src/routes/api/auth.ts @@ -13,19 +13,6 @@ import { InfoJWT } from '../../types' const authRouter = express.Router() const controller = new AuthController() -authRouter.post('/authorize', async (req, res) => { - const { error, value: body } = authorizeValidation(req.body) - if (error) return res.status(400).send(error.details[0].message) - - try { - const response = await controller.authorize(body) - - res.send(response) - } catch (err: any) { - res.status(403).send(err.toString()) - } -}) - 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/routes/web/web.ts b/api/src/routes/web/web.ts index 7cb74e1..72d7ae7 100644 --- a/api/src/routes/web/web.ts +++ b/api/src/routes/web/web.ts @@ -1,6 +1,7 @@ import express from 'express' import { WebController } from '../../controllers/web' -import { loginWebValidation } from '../../utils' +import { authenticateAccessToken } from '../../middlewares' +import { authorizeValidation, loginWebValidation } from '../../utils' const webRouter = express.Router() const controller = new WebController() @@ -17,7 +18,7 @@ webRouter.get('/', async (req, res) => { } }) -webRouter.post('/login', async (req, res) => { +webRouter.post('/SASLogon/login', async (req, res) => { const { error, value: body } = loginWebValidation(req.body) if (error) return res.status(400).send(error.details[0].message) @@ -29,6 +30,22 @@ webRouter.post('/login', async (req, res) => { } }) +webRouter.post( + '/SASLogon/authorize', + authenticateAccessToken, + async (req, res) => { + const { error, value: body } = authorizeValidation(req.body) + if (error) return res.status(400).send(error.details[0].message) + + try { + const response = await controller.authorize(req, body) + res.send(response) + } catch (err: any) { + res.status(400).send(err.toString()) + } + } +) + webRouter.get('/logout', async (req, res) => { try { await controller.logout(req) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index a18cef9..ba9898f 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -13,8 +13,6 @@ export const loginWebValidation = (data: any): Joi.ValidationResult => export const authorizeValidation = (data: any): Joi.ValidationResult => Joi.object({ - username: usernameSchema.required(), - password: passwordSchema.required(), clientId: Joi.string().required() }).validate(data)