1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +00:00

Compare commits

..

4 Commits

Author SHA1 Message Date
Saad Jutt
efacb1e916 chore(release): 0.0.12 2021-12-15 18:24:17 +05:00
Saad Jutt
d19ce253b4 fix: use env if provided for desktop mode 2021-12-15 18:24:04 +05:00
Saad Jutt
e11a4b66e7 chore(release): 0.0.11 2021-12-15 17:51:42 +05:00
Muhammad Saad
d0a1457f44 feat: added authorization route for web (#37) 2021-12-15 17:51:19 +05:00
29 changed files with 130 additions and 405 deletions

View File

@@ -2,6 +2,20 @@
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.12](https://github.com/sasjs/server/compare/v0.0.11...v0.0.12) (2021-12-15)
### Bug Fixes
* use env if provided for desktop mode ([d19ce25](https://github.com/sasjs/server/commit/d19ce253b4e2d2a7dd912d43a553d4c1bd60ba58))
### [0.0.11](https://github.com/sasjs/server/compare/v0.0.10...v0.0.11) (2021-12-15)
### Features
* added authorization route for web ([#37](https://github.com/sasjs/server/issues/37)) ([d0a1457](https://github.com/sasjs/server/commit/d0a1457f44a3d8993b57106e5e681c4e51fe8e7d))
### [0.0.10](https://github.com/sasjs/server/compare/v0.0.9...v0.0.10) (2021-12-07)
### [0.0.9](https://github.com/sasjs/server/compare/v0.0.3...v0.0.9) (2021-12-07)

View File

@@ -59,12 +59,12 @@ It will build following images if running first time:
### Using node:
#### Development (running api and web separately):
#### Development (running api and web seperately):
##### API
Navigate to `./api`
There is `.env.example` file present at `./api` directory. Remember to provide environment variables else default values will be used mentioned in `.env.example` files
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
```

View File

@@ -5,4 +5,7 @@ PORT_WEB=[port for sasjs web component(react)] default value is 3000
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
SAS_DRIVE=./tmp

View File

@@ -86,6 +86,6 @@
"typescript": "^4.3.2"
},
"configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4"
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
}
}

View File

@@ -8,8 +8,6 @@ import webRouter from './routes/web'
import apiRouter from './routes/api'
import { connectDB, getWebBuildFolderPath } from './utils'
import { FolderController } from './controllers'
dotenv.config()
const app = express()
@@ -32,8 +30,4 @@ app.use(express.json({ limit: '50mb' }))
app.use(express.static(getWebBuildFolderPath()))
const folderController = new FolderController()
folderController.addRootFolder()
export default connectDB().then(() => app)

View File

@@ -1,69 +0,0 @@
import { Body } from 'tsoa'
import Folder, { FolderPayload, MemberType } from '../model/Folder'
export class FolderController {
public async createFolder(@Body() body: FolderPayload) {
return createFolder(body)
}
public async addRootFolder() {
await addRootFolder()
}
}
interface FolderDetailsResponse {
name: string
parentFolderUri: string
children: []
}
const createFolder = async ({
name,
parentFolderUri,
type
}: FolderPayload): Promise<FolderDetailsResponse> => {
parentFolderUri = parentFolderUri.replace(/\/folders\/folders\//i, '')
const parentFolder = await Folder.findById(parentFolderUri).catch(
(_: any) => {
throw new Error(
`No folder with an URI '${parentFolderUri}' has been found.`
)
}
)
const folder = new Folder({
name,
parentFolderUri,
type
})
const savedFolder = await folder.save().catch((err: any) => {
// TODO: log error
throw new Error(`Error while saving folder.`)
})
await parentFolder?.addMember(savedFolder._id)
return {
name: savedFolder.name,
parentFolderUri: savedFolder.parentFolderUri,
children: []
}
}
const addRootFolder = async () => {
let folder = await Folder.findOne({ name: '/' })
if (folder) return
folder = new Folder({
name: '/',
parentFolderUri: '',
type: MemberType.Folder
})
folder.parentFolderUri = folder._id
return await folder.save()
}
const getItem = async({ path })

View File

@@ -5,4 +5,3 @@ export * from './group'
export * from './stp'
export * from './user'
export * from './session'
export * from './folder'

View File

@@ -1,7 +1,5 @@
import jwt from 'jsonwebtoken'
import { Request, Response } from 'express'
import { verifyTokenInDB } from '../utils'
import { headerIsNotPresentMessage, headerIsNotValidMessage } from './header'
export const authenticateAccessToken = (req: any, res: any, next: any) => {
authenticateToken(
@@ -23,18 +21,6 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => {
)
}
export const verifyAuthHeaderIsPresent = (req: Request, res: Response) => {
console.log(`🤖[verifyAuthHeaderIsPresent]🤖`)
const authHeader = req.headers.authorization
if (!authHeader) {
return res.status(401).json(headerIsNotPresentMessage('Authorization'))
} else if (!/^Bearer\s.{1}/.test(authHeader)) {
return res.status(401).json(headerIsNotValidMessage('Authorization'))
}
}
const authenticateToken = (
req: any,
res: any,

View File

@@ -1,29 +0,0 @@
import { Request, Response } from 'express'
export const verifyAcceptHeader = (req: Request, res: Response) => {
const acceptHeader = req.headers.accept
if (!acceptHeader) {
return res.status(406).json(headerIsNotPresentMessage('Accept'))
} else if (acceptHeader !== 'application/json') {
return res.status(406).json(headerIsNotValidMessage('Accept'))
}
}
export const verifyContentTypeHeader = (req: Request, res: Response) => {
const contentTypeHeader = req.headers['content-type']
if (!contentTypeHeader) {
return res.status(406).json(headerIsNotPresentMessage('Content-Type'))
} else if (contentTypeHeader !== 'application/json') {
return res.status(406).json(headerIsNotValidMessage('Content-Type'))
}
}
export const headerIsNotPresentMessage = (header: string) => ({
message: `${header} header is not present.`
})
export const headerIsNotValidMessage = (header: string) => ({
message: `${header} header is not valid.`
})

View File

@@ -2,5 +2,3 @@ export * from './authenticateToken'
export * from './desktop'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'
export * from './header'
export * from './mock'

View File

@@ -1,24 +0,0 @@
import {
verifyAuthHeaderIsPresent,
verifyAcceptHeader,
verifyContentTypeHeader
} from './'
import { Request, Response, NextFunction } from 'express'
export const verifyHeaders = (
req: Request,
res: Response,
next: NextFunction
) => {
switch (true) {
case verifyAuthHeaderIsPresent(req, res) !== undefined:
break
case verifyAcceptHeader(req, res) !== undefined:
break
case verifyContentTypeHeader(req, res) !== undefined:
break
default:
return next()
}
}

View File

@@ -1,80 +0,0 @@
import { Document, Schema, Model, model } from 'mongoose'
import {} from '@sasjs/utils'
export interface FolderPayload {
parentFolderUri: string
name: string
type: MemberType
}
export enum MemberType {
Folder = 'Folder',
File = 'File'
}
const isMemberType = (value: string) => value in MemberType
export const getMemberType = (value: string) => {
value
}
interface IFolderDocument extends FolderPayload, Document {
members: Schema.Types.ObjectId[]
type: MemberType
}
interface IFolder extends IFolderDocument {
addMember(memberId: Schema.Types.ObjectId): Promise<IFolder>
}
interface IFolderModel extends Model<IFolder> {}
const folderSchema = new Schema({
name: { type: String, required: true },
parentFolderUri: { type: String, required: true },
members: [{ type: Schema.Types.ObjectId, refPath: 'member' }],
type: { type: String, required: true }
})
folderSchema.post('save', (folder: IFolder, next: Function) => {
folder.populate('members', '').then(() => next())
next()
})
// folderSchema.get('item', (folder: IFolder, next: Function) => {
// next()
// })
folderSchema.method(
'addMember',
async function (memberId: Schema.Types.ObjectId) {
const folderIdIndex = this.members.indexOf(memberId)
if (folderIdIndex === -1) this.members.push(memberId)
this.markModified('folders')
return this.save()
}
)
folderSchema.method('getItem', async function (path: string) {
console.log(`🤖[getItem]🤖`)
console.log(`🤖[path]🤖`, path)
// const folderIdIndex = this.members.indexOf(memberId)
// if (folderIdIndex === -1) this.members.push(memberId)
// this.markModified('folders')
// return this.save()
})
export const Folder: IFolderModel = model<IFolder, IFolderModel>(
'Folder',
folderSchema
)
export default Folder

View File

@@ -8,7 +8,11 @@ import {
authenticateRefreshToken
} from '../../middlewares'
import { authorizeValidation, tokenValidation } from '../../utils'
import {
authorizeValidation,
getDesktopFields,
tokenValidation
} from '../../utils'
import { InfoJWT } from '../../types'
const authRouter = express.Router()

View File

@@ -1,67 +0,0 @@
import express from 'express'
import { verifyHeaders } from '../../middlewares'
import { verifyQuery, setHeaders } from '../../utils'
import { FolderController } from '../../controllers'
const foldersRouter = express.Router()
const controller = new FolderController()
// https://sas.analytium.co.uk/folders/folders?parentFolderUri=/folders/folders/9e442a90-2c5b-40bb-982a-5fe3ff8a66b7
foldersRouter.post('/folders', verifyHeaders, async (req, res) => {
console.log(`🤖[req.query]🤖`, req.query)
console.log(`🤖[req.body]🤖`, req.body)
try {
const response = await controller.createFolder({
...req.query,
...req.body
})
console.log(`🤖[response]🤖`, response)
res.send(response)
} catch (err: any) {
console.log(`🤖[error]🤖`, err)
res.status(403).send(err.toString())
}
})
foldersRouter.get('/folders/@item', verifyHeaders, async (req, res) => {
const queryParam = 'path'
try {
const response = await controller.getItem({
...req.query,
...req.body
})
console.log(`🤖[response]🤖`, response)
res.send(response)
} catch (err: any) {
console.log(`🤖[error]🤖`, err)
res.status(403).send(err.toString())
}
// if (verifyQuery(req, res, [queryParam])) {
// const folderExist = Math.random() > 0.5
// setHeaders(res, folderExist)
// if (folderExist) {
// res.status(200).json({ message: 'Folder exists!' })
// } else {
// res.status(404).json({
// errorCode: 11512,
// message: 'No folders match the search criteria.',
// details: [`${queryParam}: ${req.query[queryParam]}`],
// links: [],
// version: 2
// })
// }
// }
})
export default foldersRouter

View File

@@ -16,7 +16,6 @@ import groupRouter from './group'
import clientRouter from './client'
import authRouter from './auth'
import sessionRouter from './session'
import foldersRouter from './folders'
const router = express.Router()
@@ -33,7 +32,6 @@ router.use('/drive', authenticateAccessToken, driveRouter)
router.use('/group', desktopRestrict, groupRouter)
router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/user', desktopRestrict, userRouter)
router.use('/folders', foldersRouter)
router.use(
'/',
swaggerUi.serve,

View File

@@ -1,7 +1,7 @@
declare namespace NodeJS {
export interface Process {
sasLoc: string
driveLoc?: string
driveLoc: string
sessionController?: import('../controllers/internal').SessionController
}
}

View File

@@ -3,12 +3,14 @@ import mongoose from 'mongoose'
import { configuration } from '../../package.json'
import { getDesktopFields } from '.'
import { populateClients } from '../routes/api/auth'
import { getRealPath } from '@sasjs/utils'
export const connectDB = async () => {
// NOTE: when exporting app.js as agent for supertest
// we should exlcude connecting to the real database
if (process.env.NODE_ENV !== 'test') {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
@@ -16,16 +18,19 @@ export const connectDB = async () => {
process.sasLoc = sasLoc
process.driveLoc = driveLoc
} else {
const { SAS_PATH, DRIVE_PATH } = process.env
return
process.sasLoc = SAS_PATH ?? configuration.sasPath
process.driveLoc = getRealPath(
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
)
}
const { SAS_PATH } = process.env
const sasDir = SAS_PATH ?? configuration.sasPath
process.sasLoc = path.join(sasDir, 'sas')
console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc)
if (MODE?.trim() !== 'server') return
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err

View File

@@ -1,5 +1,4 @@
import path from 'path'
import { getRealPath } from '@sasjs/utils'
export const apiRoot = path.join(__dirname, '..', '..')
export const codebaseRoot = path.join(apiRoot, '..')
@@ -12,8 +11,7 @@ export const sysInitCompiledPath = path.join(
export const getWebBuildFolderPath = () =>
path.join(codebaseRoot, 'web', 'build')
export const getTmpFolderPath = () =>
process.driveLoc ?? getRealPath(path.join(process.cwd(), 'tmp'))
export const getTmpFolderPath = () => process.driveLoc
export const getTmpFilesFolderPath = () =>
path.join(getTmpFolderPath(), 'files')

View File

@@ -5,8 +5,10 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
const isWindows = () => process.platform === 'win32'
export const getDesktopFields = async () => {
const sasLoc = await getSASLocation()
const driveLoc = await getDriveLocation()
const { SAS_PATH, DRIVE_PATH } = process.env
const sasLoc = SAS_PATH ?? (await getSASLocation())
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
return { sasLoc, driveLoc }
}

View File

@@ -10,4 +10,3 @@ export * from './sleep'
export * from './upload'
export * from './validation'
export * from './verifyTokenInDB'
export * from './mock'

View File

@@ -1,38 +0,0 @@
import { Response } from 'express'
import { uuidv4 } from '@sasjs/utils'
export const setHeaders = (res: Response, isSuccess: boolean) => {
res.setHeader(
'cache-control',
`no-cache, no-store, max-age=0, must-revalidate`
)
res.setHeader(
'content-security-policy',
`default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' *.sas.com blob: data:; style-src 'self' 'unsafe-inline'; child-src 'self' blob: data: mailto:;`
)
res.setHeader(
'content-type',
`application/vnd.sas.${isSuccess ? 'content.folder' : 'error'}+json${
isSuccess ? '' : '; version=2;charset=UTF-8'
}`
)
res.setHeader('pragma', `no-cache`)
res.setHeader('server', `Apache/2.4`)
res.setHeader('strict-transport-security', `max-age=31536000`)
res.setHeader('Transfer-Encoding', `chunked`)
res.setHeader('vary', `User-Agent`)
res.setHeader('x-content-type-options', `nosniff`)
res.setHeader('x-frame-options', `SAMEORIGIN`)
res.setHeader('x-xss-protection', `1; mode=block`)
if (isSuccess) {
const uuid = uuidv4()
res.setHeader('content-location', `/folders/folders/${uuid}`)
res.setHeader('etag', `-2066812946`)
res.setHeader('last-modified', `${new Date(Date.now()).toUTCString()}`)
res.setHeader('location', `/folders/folders/${uuid}`)
} else {
res.setHeader('sas-service-response-flag', `true`)
}
}

View File

@@ -1,2 +0,0 @@
export * from './query'
export * from './header'

View File

@@ -1,18 +0,0 @@
import { Request, Response } from 'express'
export const verifyQuery = (req: Request, res: Response, args: string[]) => {
let isValid = true
const { query } = req
args.forEach((arg: string) => {
if (!Object.keys(query).includes(arg)) {
res.status(400).json({ message: `${arg} query argument is not present.` })
isValid = false
} else if (!query[arg]) {
res.status(400).json({ message: `${arg} query argument is not valid.` })
isValid = false
}
})
return isValid
}

View File

@@ -7,7 +7,7 @@ services:
context: .
dockerfile: DockerfileApi
environment:
MODE: ${MODE}
MODE: 'server'
CORS: ${CORS}
PORT: ${PORT_API}
PORT_WEB: ${PORT_WEB}

16
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "server",
"version": "0.0.10",
"version": "0.0.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "server",
"version": "0.0.10",
"version": "0.0.12",
"devDependencies": {
"prettier": "^2.3.1",
"standard-version": "^9.3.2"
@@ -1350,9 +1350,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"node_modules/minimist-options": {
@@ -3158,9 +3158,9 @@
}
},
"minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"minimist-options": {

View File

@@ -1,6 +1,6 @@
{
"name": "server",
"version": "0.0.10",
"version": "0.0.12",
"description": "NodeJS wrapper for calling the SAS binary executable",
"scripts": {
"server": "npm run server:prepare && npm run server:start",

View File

@@ -19,7 +19,14 @@ function App() {
<ThemeProvider theme={theme}>
<HashRouter>
<Header />
<Login setTokens={setTokens} />
<Switch>
<Route exact path="/SASjsLogon">
<Login getCodeOnly />
</Route>
<Route path="/">
<Login setTokens={setTokens} />
</Route>
</Switch>
</HashRouter>
</ThemeProvider>
)
@@ -39,6 +46,9 @@ function App() {
<Route exact path="/SASjsStudio">
<Studio />
</Route>
<Route exact path="/SASjsLogon">
<Login getCodeOnly />
</Route>
</Switch>
</HashRouter>
</ThemeProvider>

View File

@@ -1,7 +1,8 @@
import React, { useState } from 'react'
import { useLocation } from 'react-router-dom'
import PropTypes from 'prop-types'
import { CssBaseline, Box, TextField, Button } from '@mui/material'
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
const headers = {
Accept: 'application/json',
@@ -18,7 +19,12 @@ const getAuthCode = async (credentials: any) => {
method: 'POST',
headers,
body: JSON.stringify(credentials)
}).then((data) => data.json())
}).then(async (response) => {
const resText = await response.text()
if (response.status !== 200) throw resText
return JSON.parse(resText)
})
}
const getTokens = async (payload: any) => {
return fetch(`${baseUrl}/SASjsApi/auth/token`, {
@@ -28,26 +34,62 @@ const getTokens = async (payload: any) => {
}).then((data) => data.json())
}
const Login = ({ setTokens }: any) => {
const [username, setUserName] = useState()
const [password, setPassword] = useState()
const Login = ({ setTokens, getCodeOnly }: any) => {
const location = useLocation()
const [username, setUserName] = useState('')
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState('')
let error: boolean
const [displayCode, setDisplayCode] = useState(null)
const handleSubmit = async (e: any) => {
error = false
setErrorMessage('')
e.preventDefault()
const { REACT_APP_CLIENT_ID: clientId } = process.env
let { REACT_APP_CLIENT_ID: clientId } = process.env
if (getCodeOnly) {
const params = new URLSearchParams(location.search)
const responseType = params.get('response_type')
if (responseType === 'code')
clientId = params.get('client_id') ?? undefined
}
const { code } = await getAuthCode({
clientId,
username,
password
}).catch((err: string) => {
error = true
setErrorMessage(err)
return {}
})
const { accessToken, refreshToken } = await getTokens({
clientId,
code
})
if (!error) {
if (getCodeOnly) return setDisplayCode(code)
setTokens(accessToken, refreshToken)
const { accessToken, refreshToken } = await getTokens({
clientId,
code
})
setTokens(accessToken, refreshToken)
}
}
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 (
@@ -61,7 +103,12 @@ const Login = ({ setTokens }: any) => {
>
<CssBaseline />
<br />
<h2>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
@@ -80,6 +127,7 @@ const Login = ({ setTokens }: any) => {
onChange={(e: any) => setPassword(e.target.value)}
required
/>
{errorMessage && <span>{errorMessage}</span>}
<Button type="submit" variant="outlined">
Submit
</Button>
@@ -88,7 +136,8 @@ const Login = ({ setTokens }: any) => {
}
Login.propTypes = {
setTokens: PropTypes.func.isRequired
setTokens: PropTypes.func,
getCodeOnly: PropTypes.bool
}
export default Login

View File

@@ -3,15 +3,8 @@ import { useEffect, useState } from 'react'
export default function useTokens() {
const getTokens = () => {
const accessTokenString = localStorage.getItem('accessToken')
const accessToken: string = accessTokenString
? JSON.parse(accessTokenString)
: undefined
const refreshTokenString = localStorage.getItem('refreshToken')
const refreshToken: string = refreshTokenString
? JSON.parse(refreshTokenString)
: undefined
const accessToken = localStorage.getItem('accessToken')
const refreshToken = localStorage.getItem('refreshToken')
if (accessToken && refreshToken) {
setAxiosRequestHeader(accessToken)
@@ -31,8 +24,8 @@ export default function useTokens() {
setAxiosResponse(setTokens)
const saveTokens = (accessToken: string, refreshToken: string) => {
localStorage.setItem('accessToken', JSON.stringify(accessToken))
localStorage.setItem('refreshToken', JSON.stringify(refreshToken))
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
setAxiosRequestHeader(accessToken)
setTokens({ accessToken, refreshToken })
}