diff --git a/api/mocks/generic/sas9/public-access-denied b/api/mocks/generic/sas9/public-access-denied new file mode 100644 index 0000000..6efd19e --- /dev/null +++ b/api/mocks/generic/sas9/public-access-denied @@ -0,0 +1 @@ +Public access has been denied. \ No newline at end of file diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 7467ba6..8250128 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -62,48 +62,6 @@ components: - clientSecret type: object additionalProperties: false - IRecordOfAny: - properties: {} - type: object - additionalProperties: {} - LogLine: - properties: - line: - type: string - required: - - line - type: object - additionalProperties: false - HTTPHeaders: - properties: {} - type: object - additionalProperties: - type: string - ExecuteReturnJsonResponse: - properties: - status: - type: string - _webout: - anyOf: - - - type: string - - - $ref: '#/components/schemas/IRecordOfAny' - log: - items: - $ref: '#/components/schemas/LogLine' - type: array - message: - type: string - httpHeaders: - $ref: '#/components/schemas/HTTPHeaders' - required: - - status - - _webout - - log - - httpHeaders - type: object - additionalProperties: false RunTimeType: enum: - sas @@ -550,7 +508,7 @@ components: - setting type: object additionalProperties: false - ExecuteReturnJsonPayload: + ExecutePostRequestPayload: properties: _program: type: string @@ -698,7 +656,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExecuteReturnJsonResponse' + anyOf: + - {type: string} + - {type: string, format: byte} description: 'Execute SAS code.' summary: 'Run SAS Code and returns log' tags: @@ -1687,7 +1647,7 @@ paths: parameters: [] /SASjsApi/stp/execute: get: - operationId: ExecuteReturnRaw + operationId: ExecuteGetRequest responses: '200': description: Ok @@ -1714,17 +1674,16 @@ paths: type: string example: /Projects/myApp/some/program post: - operationId: ExecuteReturnJson + operationId: ExecutePostRequest responses: '200': description: Ok content: application/json: schema: - $ref: '#/components/schemas/ExecuteReturnJsonResponse' - examples: - 'Example 1': - value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}} + anyOf: + - {type: string} + - {type: string, format: byte} description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content." summary: 'Execute a Stored Program, return a JSON object' tags: @@ -1746,7 +1705,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExecuteReturnJsonPayload' + $ref: '#/components/schemas/ExecutePostRequestPayload' /: get: operationId: Home diff --git a/api/src/app.ts b/api/src/app.ts index a2bfdf9..40387e2 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -3,6 +3,7 @@ import express, { ErrorRequestHandler } from 'express' import csrf, { CookieOptions } from 'csurf' import cookieParser from 'cookie-parser' import dotenv from 'dotenv' +import bodyParser from 'body-parser' import { copySASjsCore, @@ -77,6 +78,15 @@ export default setProcessVariables().then(async () => { app.use(express.json({ limit: '100mb' })) app.use(express.static(path.join(__dirname, '../public'))) + // Body parser is used for decoding the formdata on POST request. + // Currently only place we use it is SAS9 Mock - POST /SASLogon/login + app.use( + bodyParser.urlencoded({ + extended: true + }) + ) + app.use(bodyParser.json()) + await setupFolders() await copySASjsCore() diff --git a/api/src/controllers/mock-sas9.ts b/api/src/controllers/mock-sas9.ts index 7136b17..8a78778 100644 --- a/api/src/controllers/mock-sas9.ts +++ b/api/src/controllers/mock-sas9.ts @@ -15,7 +15,7 @@ export interface MockFileRead { } export class MockSas9Controller { - private loggedIn: boolean = false + private loggedIn: string | undefined @Get('/SASStoredProcess') public async sasStoredProcess(): Promise { @@ -46,6 +46,13 @@ export class MockSas9Controller { } } + if (this.isPublicAccount()) { + return { + content: '', + redirect: '/SASLogon/Login' + } + } + let program = req.query._program?.toString() || '' program = program.replace('/', '') @@ -68,6 +75,23 @@ export class MockSas9Controller { @Get('/SASLogon/login') public async loginGet(): Promise { + if (this.loggedIn) { + if (this.isPublicAccount()) { + return { + content: '', + redirect: '/SASStoredProcess/Logoff?publicDenied=true' + } + } else { + return await getMockResponseFromFile([ + process.cwd(), + 'mocks', + 'generic', + 'sas9', + 'logged-in' + ]) + } + } + return await getMockResponseFromFile([ process.cwd(), 'mocks', @@ -78,8 +102,8 @@ export class MockSas9Controller { } @Post('/SASLogon/login') - public async loginPost(): Promise { - this.loggedIn = true + public async loginPost(req: express.Request): Promise { + this.loggedIn = req.body.username return await getMockResponseFromFile([ process.cwd(), @@ -91,8 +115,18 @@ export class MockSas9Controller { } @Get('/SASLogon/logout') - public async logout(): Promise { - this.loggedIn = false + public async logout(req: express.Request): Promise { + this.loggedIn = undefined + + if (req.query.publicDenied === 'true') { + return await getMockResponseFromFile([ + process.cwd(), + 'mocks', + 'generic', + 'sas9', + 'public-access-denied' + ]) + } return await getMockResponseFromFile([ process.cwd(), @@ -102,6 +136,20 @@ export class MockSas9Controller { 'logged-out' ]) } + + @Get('/SASStoredProcess/Logoff') //publicDenied=true + public async logoff(req: express.Request): Promise { + const params = req.query.publicDenied + ? `?publicDenied=${req.query.publicDenied}` + : '' + + return { + content: '', + redirect: '/SASLogon/logout' + params + } + } + + private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public' } /** diff --git a/api/src/routes/web/sas9-web.ts b/api/src/routes/web/sas9-web.ts index 09f5355..0efc6f3 100644 --- a/api/src/routes/web/sas9-web.ts +++ b/api/src/routes/web/sas9-web.ts @@ -58,6 +58,11 @@ sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => { sas9WebRouter.get('/SASLogon/login', async (req, res) => { const response = await controller.loginGet() + if (response.redirect) { + res.redirect(response.redirect) + return + } + try { res.send(response.content) } catch (err: any) { @@ -66,7 +71,12 @@ sas9WebRouter.get('/SASLogon/login', async (req, res) => { }) sas9WebRouter.post('/SASLogon/login', async (req, res) => { - const response = await controller.loginPost() + const response = await controller.loginPost(req) + + if (response.redirect) { + res.redirect(response.redirect) + return + } try { res.send(response.content) @@ -76,7 +86,27 @@ sas9WebRouter.post('/SASLogon/login', async (req, res) => { }) sas9WebRouter.get('/SASLogon/logout', async (req, res) => { - const response = await controller.logout() + const response = await controller.logout(req) + + if (response.redirect) { + res.redirect(response.redirect) + return + } + + try { + res.send(response.content) + } catch (err: any) { + res.status(403).send(err.toString()) + } +}) + +sas9WebRouter.get('/SASStoredProcess/Logoff', async (req, res) => { + const response = await controller.logoff(req) + + if (response.redirect) { + res.redirect(response.redirect) + return + } try { res.send(response.content)