mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 11:24:35 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fadcc9bd29 | ||
|
|
182def2f3e | ||
|
|
06a5f39fea | ||
|
|
143b367a0e | ||
|
|
b5fd800300 | ||
|
|
a0b52d9982 | ||
|
|
5888f04e08 | ||
|
|
b40de8fa6a |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -54,6 +54,7 @@ jobs:
|
|||||||
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
||||||
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
||||||
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
||||||
|
SESSION_SECRET: ${{secrets.SESSION_SECRET}}
|
||||||
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
working-directory: ./api
|
working-directory: ./api
|
||||||
|
|||||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [0.0.75](https://github.com/sasjs/server/compare/v0.0.69...v0.0.75) (2022-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
|
||||||
|
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
|
||||||
|
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
|
||||||
|
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
|
||||||
|
* moved getAuthCode from api to web routes ([b40de8f](https://github.com/sasjs/server/commit/b40de8fa6a5aa763ed25a6fe6a381e483e0ab824))
|
||||||
|
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
|
||||||
|
* **web:** seperate container for auth code ([5888f04](https://github.com/sasjs/server/commit/5888f04e08a32c6d2c7bcfcbc3a1d32425bff3b3))
|
||||||
|
|
||||||
### [0.0.74](https://github.com/sasjs/server/compare/v0.0.73...v0.0.74) (2022-05-12)
|
### [0.0.74](https://github.com/sasjs/server/compare/v0.0.73...v0.0.74) (2022-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
26
api/package-lock.json
generated
26
api/package-lock.json
generated
@@ -2995,14 +2995,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001243",
|
"version": "1.0.30001340",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz",
|
||||||
"integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
|
"integrity": "sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": {
|
"funding": [
|
||||||
"type": "opencollective",
|
{
|
||||||
"url": "https://opencollective.com/browserslist"
|
"type": "opencollective",
|
||||||
}
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -12712,9 +12718,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001243",
|
"version": "1.0.30001340",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz",
|
||||||
"integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
|
"integrity": "sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
|
|||||||
@@ -5,51 +5,6 @@ components:
|
|||||||
requestBodies: {}
|
requestBodies: {}
|
||||||
responses: {}
|
responses: {}
|
||||||
schemas:
|
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:
|
TokenResponse:
|
||||||
properties:
|
properties:
|
||||||
accessToken:
|
accessToken:
|
||||||
@@ -92,6 +47,41 @@ components:
|
|||||||
- userId
|
- userId
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
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:
|
ClientPayload:
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
@@ -425,14 +415,6 @@ components:
|
|||||||
- description
|
- description
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
ExecuteReturnJsonPayload:
|
|
||||||
properties:
|
|
||||||
_program:
|
|
||||||
type: string
|
|
||||||
description: 'Location of SAS program'
|
|
||||||
example: /Public/somefolder/some.file
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
InfoResponse:
|
InfoResponse:
|
||||||
properties:
|
properties:
|
||||||
mode:
|
mode:
|
||||||
@@ -452,6 +434,14 @@ components:
|
|||||||
- protocol
|
- protocol
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
ExecuteReturnJsonPayload:
|
||||||
|
properties:
|
||||||
|
_program:
|
||||||
|
type: string
|
||||||
|
description: 'Location of SAS program'
|
||||||
|
example: /Public/somefolder/some.file
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@@ -465,86 +455,6 @@ info:
|
|||||||
name: '4GL Ltd'
|
name: '4GL Ltd'
|
||||||
openapi: 3.0.0
|
openapi: 3.0.0
|
||||||
paths:
|
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:
|
/SASjsApi/auth/token:
|
||||||
post:
|
post:
|
||||||
operationId: Token
|
operationId: Token
|
||||||
@@ -602,6 +512,86 @@ paths:
|
|||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
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:
|
/SASjsApi/client:
|
||||||
post:
|
post:
|
||||||
operationId: CreateClient
|
operationId: CreateClient
|
||||||
@@ -1248,6 +1238,24 @@ paths:
|
|||||||
format: double
|
format: double
|
||||||
type: number
|
type: number
|
||||||
example: '6789'
|
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:
|
/SASjsApi/session:
|
||||||
get:
|
get:
|
||||||
operationId: Session
|
operationId: Session
|
||||||
@@ -1330,24 +1338,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
$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:
|
servers:
|
||||||
-
|
-
|
||||||
url: /
|
url: /
|
||||||
|
|||||||
@@ -85,21 +85,25 @@ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
|||||||
* With Mongo Store *
|
* With Mongo Store *
|
||||||
***********************************/
|
***********************************/
|
||||||
if (MODE?.trim() === 'server') {
|
if (MODE?.trim() === 'server') {
|
||||||
|
let store: MongoStore | undefined
|
||||||
|
|
||||||
// NOTE: when exporting app.js as agent for supertest
|
// NOTE: when exporting app.js as agent for supertest
|
||||||
// we should exclude connecting to the real database
|
// we should exclude connecting to the real database
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
||||||
|
|
||||||
app.use(
|
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
|
||||||
session({
|
|
||||||
secret: process.env.SESSION_SECRET as string,
|
|
||||||
saveUninitialized: false, // don't create session until something stored
|
|
||||||
resave: false, //don't save session if unmodified
|
|
||||||
store: MongoStore.create({ clientPromise, collectionName: 'sessions' }),
|
|
||||||
cookie: cookieOptions
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: process.env.SESSION_SECRET as string,
|
||||||
|
saveUninitialized: false, // don't create session until something stored
|
||||||
|
resave: false, //don't save session if unmodified
|
||||||
|
store,
|
||||||
|
cookie: cookieOptions
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
app.use(express.json({ limit: '100mb' }))
|
app.use(express.json({ limit: '100mb' }))
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import User from '../model/User'
|
|
||||||
import Client from '../model/Client'
|
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
generateAuthCode,
|
|
||||||
generateRefreshToken,
|
generateRefreshToken,
|
||||||
removeTokensInDB,
|
removeTokensInDB,
|
||||||
saveTokensInDB
|
saveTokensInDB
|
||||||
@@ -25,20 +22,6 @@ export class AuthController {
|
|||||||
static deleteCode = (userId: number, clientId: string) =>
|
static deleteCode = (userId: number, clientId: string) =>
|
||||||
delete AuthController.authCodes[userId][clientId]
|
delete AuthController.authCodes[userId][clientId]
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<AuthorizeResponse>({
|
|
||||||
code: 'someRandomCryptoString'
|
|
||||||
})
|
|
||||||
@Post('/authorize')
|
|
||||||
public async authorize(
|
|
||||||
@Body() body: AuthorizePayload
|
|
||||||
): Promise<AuthorizeResponse> {
|
|
||||||
return authorize(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Accepts client/auth code and returns access/refresh tokens
|
* @summary Accepts client/auth code and returns access/refresh tokens
|
||||||
*
|
*
|
||||||
@@ -79,33 +62,6 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorize = async (data: any): Promise<AuthorizeResponse> => {
|
|
||||||
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<TokenResponse> => {
|
const token = async (data: any): Promise<TokenResponse> => {
|
||||||
const { clientId, code } = data
|
const { clientId, code } = data
|
||||||
|
|
||||||
@@ -143,32 +99,6 @@ const logout = async (userInfo: InfoJWT) => {
|
|||||||
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
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 {
|
interface TokenPayload {
|
||||||
/**
|
/**
|
||||||
* Client ID
|
* Client ID
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ export * from './client'
|
|||||||
export * from './code'
|
export * from './code'
|
||||||
export * from './drive'
|
export * from './drive'
|
||||||
export * from './group'
|
export * from './group'
|
||||||
|
export * from './info'
|
||||||
export * from './session'
|
export * from './session'
|
||||||
export * from './stp'
|
export * from './stp'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './info'
|
export * from './web'
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express from 'express'
|
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 { readFile } from '@sasjs/utils'
|
||||||
|
|
||||||
import User from '../model/User'
|
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('/')
|
@Route('/')
|
||||||
@Tags('Web')
|
@Tags('Web')
|
||||||
@@ -22,7 +25,7 @@ export class WebController {
|
|||||||
* @summary Accept a valid username/password
|
* @summary Accept a valid username/password
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Post('/login')
|
@Post('/SASLogon/login')
|
||||||
public async login(
|
public async login(
|
||||||
@Request() req: express.Request,
|
@Request() req: express.Request,
|
||||||
@Body() body: LoginPayload
|
@Body() body: LoginPayload
|
||||||
@@ -30,6 +33,21 @@ export class WebController {
|
|||||||
return login(req, body)
|
return login(req, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<AuthorizeResponse>({
|
||||||
|
code: 'someRandomCryptoString'
|
||||||
|
})
|
||||||
|
@Post('/SASLogon/authorize')
|
||||||
|
public async authorize(
|
||||||
|
@Request() req: express.Request,
|
||||||
|
@Body() body: AuthorizePayload
|
||||||
|
): Promise<AuthorizeResponse> {
|
||||||
|
return authorize(req, body.clientId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Accept a valid username/password
|
* @summary Accept a valid username/password
|
||||||
*
|
*
|
||||||
@@ -84,6 +102,30 @@ const login = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authorize = async (
|
||||||
|
req: express.Request,
|
||||||
|
clientId: string
|
||||||
|
): Promise<AuthorizeResponse> => {
|
||||||
|
const userId = req.session.user?.userId
|
||||||
|
if (!userId) throw new Error('Invalid userId.')
|
||||||
|
|
||||||
|
const client = await Client.findOne({ clientId })
|
||||||
|
if (!client) throw new Error('Invalid clientId.')
|
||||||
|
|
||||||
|
// generate authorization code against clientId
|
||||||
|
const userInfo: InfoJWT = {
|
||||||
|
clientId,
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
const code = AuthController.saveCode(
|
||||||
|
userId,
|
||||||
|
clientId,
|
||||||
|
generateAuthCode(userInfo)
|
||||||
|
)
|
||||||
|
|
||||||
|
return { code }
|
||||||
|
}
|
||||||
|
|
||||||
interface LoginPayload {
|
interface LoginPayload {
|
||||||
/**
|
/**
|
||||||
* Username for user
|
* Username for user
|
||||||
@@ -96,3 +138,19 @@ interface LoginPayload {
|
|||||||
*/
|
*/
|
||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthorizePayload {
|
||||||
|
/**
|
||||||
|
* Client ID
|
||||||
|
* @example "clientID1"
|
||||||
|
*/
|
||||||
|
clientId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthorizeResponse {
|
||||||
|
/**
|
||||||
|
* Authorization code
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,19 +13,6 @@ import { InfoJWT } from '../../types'
|
|||||||
const authRouter = express.Router()
|
const authRouter = express.Router()
|
||||||
const controller = new AuthController()
|
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) => {
|
authRouter.post('/token', async (req, res) => {
|
||||||
const { error, value: body } = tokenValidation(req.body)
|
const { error, value: body } = tokenValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|||||||
@@ -49,114 +49,6 @@ describe('auth', () => {
|
|||||||
await mongoServer.stop()
|
await mongoServer.stop()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authorize', () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
const collections = mongoose.connection.collections
|
|
||||||
const collection = collections['users']
|
|
||||||
await collection.deleteMany({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with authorization code', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password,
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body).toHaveProperty('code')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if username is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
password: user.password,
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"username" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if password is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"password" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if clientId is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"clientId" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Forbidden if username is incorrect', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password,
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Username is not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Forbidden if password is incorrect', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: 'WrongPassword',
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Invalid password.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password,
|
|
||||||
clientId: 'WrongClientID'
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Invalid clientId.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('token', () => {
|
describe('token', () => {
|
||||||
const userInfo: InfoJWT = {
|
const userInfo: InfoJWT = {
|
||||||
clientId,
|
clientId,
|
||||||
|
|||||||
182
api/src/routes/api/spec/web.spec.ts
Normal file
182
api/src/routes/api/spec/web.spec.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
|
import request from 'supertest'
|
||||||
|
import appPromise from '../../../app'
|
||||||
|
import { UserController, ClientController } from '../../../controllers/'
|
||||||
|
|
||||||
|
const clientId = 'someclientID'
|
||||||
|
const clientSecret = 'someclientSecret'
|
||||||
|
const user = {
|
||||||
|
id: 1234,
|
||||||
|
displayName: 'Test User',
|
||||||
|
username: 'testUsername',
|
||||||
|
password: '87654321',
|
||||||
|
isAdmin: false,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('web', () => {
|
||||||
|
let app: Express
|
||||||
|
let con: Mongoose
|
||||||
|
let mongoServer: MongoMemoryServer
|
||||||
|
const userController = new UserController()
|
||||||
|
const clientController = new ClientController()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
|
mongoServer = await MongoMemoryServer.create()
|
||||||
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
await clientController.createClient({ clientId, clientSecret })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await con.connection.dropDatabase()
|
||||||
|
await con.connection.close()
|
||||||
|
await mongoServer.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('home', () => {
|
||||||
|
it('should respond with CSRF Token', async () => {
|
||||||
|
await request(app)
|
||||||
|
.get('/')
|
||||||
|
.expect(
|
||||||
|
'set-cookie',
|
||||||
|
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SASLogon/login', () => {
|
||||||
|
let csrfToken: string
|
||||||
|
let cookies: string
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ csrfToken, cookies } = await getCSRF(app))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const collections = mongoose.connection.collections
|
||||||
|
const collection = collections['users']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with successful login', async () => {
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('Cookie', cookies)
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.loggedIn).toBeTruthy()
|
||||||
|
expect(res.body.user).toEqual({
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SASLogon/authorize', () => {
|
||||||
|
let csrfToken: string
|
||||||
|
let cookies: string
|
||||||
|
let authCookies: string
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ csrfToken, cookies } = await getCSRF(app))
|
||||||
|
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
const credentials = {
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
}
|
||||||
|
|
||||||
|
;({ cookies: authCookies } = await performLogin(
|
||||||
|
app,
|
||||||
|
credentials,
|
||||||
|
cookies,
|
||||||
|
csrfToken
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const collections = mongoose.connection.collections
|
||||||
|
const collection = collections['users']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with authorization code', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies, cookies].join('; '))
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({ clientId })
|
||||||
|
|
||||||
|
expect(res.body).toHaveProperty('code')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if clientId is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies, cookies].join('; '))
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"clientId" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies, cookies].join('; '))
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
clientId: 'WrongClientID'
|
||||||
|
})
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Invalid clientId.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const getCSRF = async (app: Express) => {
|
||||||
|
// make request to get CSRF
|
||||||
|
const { header } = await request(app).get('/')
|
||||||
|
const cookies = header['set-cookie'].join()
|
||||||
|
|
||||||
|
console.log('cookies', cookies)
|
||||||
|
const csrfToken = extractCSRF(cookies)
|
||||||
|
return { csrfToken, cookies }
|
||||||
|
}
|
||||||
|
|
||||||
|
const performLogin = async (
|
||||||
|
app: Express,
|
||||||
|
credentials: { username: string; password: string },
|
||||||
|
cookies: string,
|
||||||
|
csrfToken: string
|
||||||
|
) => {
|
||||||
|
const { header } = await request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('Cookie', cookies)
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send(credentials)
|
||||||
|
|
||||||
|
const newCookies: string = header['set-cookie'].join()
|
||||||
|
return { cookies: newCookies }
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractCSRF = (cookies: string) =>
|
||||||
|
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
|
||||||
|
cookies
|
||||||
|
)![2]
|
||||||
@@ -1,23 +1,25 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { WebController } from '../../controllers/web'
|
import { WebController } from '../../controllers/web'
|
||||||
import { loginWebValidation } from '../../utils'
|
import { authenticateAccessToken } from '../../middlewares'
|
||||||
|
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||||
|
|
||||||
const webRouter = express.Router()
|
const webRouter = express.Router()
|
||||||
const controller = new WebController()
|
const controller = new WebController()
|
||||||
|
|
||||||
webRouter.get('/', async (req, res) => {
|
webRouter.get('/', async (req, res) => {
|
||||||
|
let response
|
||||||
try {
|
try {
|
||||||
const response = await controller.home()
|
response = await controller.home()
|
||||||
|
} catch (_) {
|
||||||
|
response = 'Web Build is not present'
|
||||||
|
} finally {
|
||||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||||
|
|
||||||
return res.send(response)
|
return res.send(response)
|
||||||
} catch (_) {
|
|
||||||
return res.send('Web Build is not present')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
webRouter.post('/login', async (req, res) => {
|
webRouter.post('/SASLogon/login', async (req, res) => {
|
||||||
const { error, value: body } = loginWebValidation(req.body)
|
const { error, value: body } = loginWebValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
@@ -25,16 +27,32 @@ webRouter.post('/login', async (req, res) => {
|
|||||||
const response = await controller.login(req, body)
|
const response = await controller.login(req, body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(400).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
webRouter.get('/logout', async (req, res) => {
|
webRouter.get('/logout', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await controller.logout(req)
|
await controller.logout(req)
|
||||||
res.status(200).send('OK!')
|
res.status(200).send('OK!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(400).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export const loginWebValidation = (data: any): Joi.ValidationResult =>
|
|||||||
|
|
||||||
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
username: usernameSchema.required(),
|
|
||||||
password: passwordSchema.required(),
|
|
||||||
clientId: Joi.string().required()
|
clientId: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.74",
|
"version": "0.0.75",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.74",
|
"version": "0.0.75",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"standard-version": "^9.3.2"
|
"standard-version": "^9.3.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.74",
|
"version": "0.0.75",
|
||||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||||
"repository": "https://github.com/sasjs/server",
|
"repository": "https://github.com/sasjs/server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Drive from './containers/Drive'
|
|||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
|
|
||||||
import { AppContext } from './context/appContext'
|
import { AppContext } from './context/appContext'
|
||||||
|
import AuthCode from './containers/AuthCode'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
@@ -20,9 +21,6 @@ function App() {
|
|||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Header />
|
<Header />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/SASjsLogon">
|
|
||||||
<Login getCodeOnly />
|
|
||||||
</Route>
|
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Login />
|
<Login />
|
||||||
</Route>
|
</Route>
|
||||||
@@ -47,7 +45,7 @@ function App() {
|
|||||||
<Studio />
|
<Studio />
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/SASjsLogon">
|
<Route exact path="/SASjsLogon">
|
||||||
<Login getCodeOnly />
|
<AuthCode />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|||||||
@@ -1,56 +1,27 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import React, { useState, useContext } from 'react'
|
import React, { useState, useContext } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
|
import { CssBaseline, Box, TextField, Button } from '@mui/material'
|
||||||
import { AppContext } from '../context/appContext'
|
import { AppContext } from '../context/appContext'
|
||||||
|
|
||||||
const getAuthCode = async (credentials: any) =>
|
|
||||||
axios.post('/SASjsApi/auth/authorize', credentials).then((res) => res.data)
|
|
||||||
|
|
||||||
const login = async (payload: { username: string; password: string }) =>
|
const login = async (payload: { username: string; password: string }) =>
|
||||||
axios.post('/login', payload).then((res) => res.data)
|
axios.post('/SASLogon/login', payload).then((res) => res.data)
|
||||||
|
|
||||||
const Login = ({ getCodeOnly }: any) => {
|
const Login = () => {
|
||||||
const location = useLocation()
|
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
let error: boolean
|
|
||||||
const [displayCode, setDisplayCode] = useState(null)
|
|
||||||
|
|
||||||
const handleSubmit = async (e: any) => {
|
const handleSubmit = async (e: any) => {
|
||||||
error = false
|
|
||||||
setErrorMessage('')
|
setErrorMessage('')
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (getCodeOnly) {
|
|
||||||
const params = new URLSearchParams(location.search)
|
|
||||||
const responseType = params.get('response_type')
|
|
||||||
if (responseType === 'code') {
|
|
||||||
const clientId = params.get('client_id')
|
|
||||||
|
|
||||||
const { code } = await getAuthCode({
|
|
||||||
clientId,
|
|
||||||
username,
|
|
||||||
password
|
|
||||||
}).catch((err: any) => {
|
|
||||||
error = true
|
|
||||||
setErrorMessage(err.response.data)
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
if (!error) return setDisplayCode(code)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { loggedIn, user } = await login({
|
const { loggedIn, user } = await login({
|
||||||
username,
|
username,
|
||||||
password
|
password
|
||||||
}).catch((err: any) => {
|
}).catch((err: any) => {
|
||||||
error = true
|
|
||||||
setErrorMessage(err.response.data)
|
setErrorMessage(err.response.data)
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
@@ -62,21 +33,6 @@ const Login = ({ getCodeOnly }: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (displayCode) {
|
|
||||||
return (
|
|
||||||
<Box className="main">
|
|
||||||
<CssBaseline />
|
|
||||||
<br />
|
|
||||||
<h2>Authorization Code</h2>
|
|
||||||
<Typography m={2} p={3} style={{ overflowWrap: 'anywhere' }}>
|
|
||||||
{displayCode}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="main"
|
className="main"
|
||||||
@@ -89,13 +45,6 @@ const Login = ({ getCodeOnly }: any) => {
|
|||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<br />
|
<br />
|
||||||
<h2 style={{ width: 'auto' }}>Welcome to SASjs Server!</h2>
|
<h2 style={{ width: 'auto' }}>Welcome to SASjs Server!</h2>
|
||||||
{getCodeOnly && (
|
|
||||||
<p style={{ width: 'auto' }}>
|
|
||||||
Provide credentials to get authorization code.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
id="username"
|
id="username"
|
||||||
label="Username"
|
label="Username"
|
||||||
|
|||||||
63
web/src/containers/AuthCode/index.tsx
Normal file
63
web/src/containers/AuthCode/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { CssBaseline, Box, Typography } from '@mui/material'
|
||||||
|
|
||||||
|
const getAuthCode = async (credentials: any) =>
|
||||||
|
axios.post('/SASLogon/authorize', credentials).then((res) => res.data)
|
||||||
|
|
||||||
|
const AuthCode = () => {
|
||||||
|
const location = useLocation()
|
||||||
|
const [displayCode, setDisplayCode] = useState(null)
|
||||||
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
requestAuthCode()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const requestAuthCode = async () => {
|
||||||
|
setErrorMessage('')
|
||||||
|
|
||||||
|
const params = new URLSearchParams(location.search)
|
||||||
|
|
||||||
|
const responseType = params.get('response_type')
|
||||||
|
if (responseType !== 'code')
|
||||||
|
return setErrorMessage('response type is not support')
|
||||||
|
|
||||||
|
const clientId = params.get('client_id')
|
||||||
|
if (!clientId) return setErrorMessage('clientId is not provided')
|
||||||
|
|
||||||
|
setErrorMessage('Fetching auth code... ')
|
||||||
|
const { code } = await getAuthCode({
|
||||||
|
clientId
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
setErrorMessage('')
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setErrorMessage(err.response.data)
|
||||||
|
return { code: null }
|
||||||
|
})
|
||||||
|
return setDisplayCode(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="main">
|
||||||
|
<CssBaseline />
|
||||||
|
<br />
|
||||||
|
<h2>Authorization Code</h2>
|
||||||
|
{displayCode && (
|
||||||
|
<Typography m={2} p={3} style={{ overflowWrap: 'anywhere' }}>
|
||||||
|
{displayCode}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{errorMessage && <Typography>{errorMessage}</Typography>}
|
||||||
|
|
||||||
|
<br />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthCode
|
||||||
Reference in New Issue
Block a user