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

chore: swagger docs generated

This commit is contained in:
Saad Jutt
2021-11-04 18:47:40 +05:00
parent 728f277f5c
commit 882f36d30e
21 changed files with 1811 additions and 282 deletions

1
.gitignore vendored
View File

@@ -6,5 +6,6 @@ node_modules/
sas/
tmp/
build/
public/
certificates/
.env

1572
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,9 +4,11 @@
"description": "SASjs server",
"main": "./src/server.ts",
"scripts": {
"prestart": "npm run swagger",
"start": "nodemon ./src/server.ts",
"start:prod": "nodemon ./src/prod-server.ts",
"build": "rimraf build && tsc",
"swagger": "tsoa spec",
"semantic-release": "semantic-release -d",
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
"test": "mkdir -p tmp && jest --coverage",
@@ -27,16 +29,21 @@
"joi": "^17.4.2",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.0.12",
"multer": "^1.4.3"
"morgan": "^1.10.0",
"multer": "^1.4.3",
"swagger-ui-express": "^4.1.6",
"tsoa": "^3.14.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/express": "^4.17.12",
"@types/jest": "^26.0.24",
"@types/jsonwebtoken": "^8.5.5",
"@types/morgan": "^1.9.3",
"@types/multer": "^1.4.7",
"@types/node": "^15.12.2",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
"dotenv": "^10.0.0",
"jest": "^27.0.6",
"mongodb-memory-server": "^8.0.0",

View File

@@ -1,10 +1,13 @@
import express from 'express'
import morgan from 'morgan'
import webRouter from './routes/web'
import apiRouter from './routes/api'
const app = express()
app.use(express.json({ limit: '50mb' }))
app.use(morgan('tiny'))
app.use(express.static('public'))
app.use('/', webRouter)
app.use('/SASjsApi', apiRouter)

View File

@@ -1,32 +0,0 @@
import bcrypt from 'bcryptjs'
import User from '../model/User'
export const createUser = async (data: any) => {
const { displayName, username, password, isAdmin, isActive } = data
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) throw new Error('Username already exists.')
// Hash passwords
const salt = await bcrypt.genSalt(10)
const hashPassword = await bcrypt.hash(password, salt)
// Create a new user
const user = new User({
displayName,
username,
password: hashPassword,
isAdmin,
isActive
})
const savedUser = await user.save()
return {
displayName: savedUser.displayName,
username: savedUser.username,
isAdmin: savedUser.isAdmin,
isActive: savedUser.isActive
}
}

View File

@@ -1,20 +0,0 @@
import bcrypt from 'bcryptjs'
import User from '../model/User'
export const deleteUser = async (
username: string,
isAdmin: boolean,
data: any
) => {
const { password } = data
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
if (!isAdmin) {
const validPass = await bcrypt.compare(password, user.password)
if (!validPass) throw new Error('Invalid password.')
}
await User.deleteOne({ username })
}

View File

@@ -1,35 +0,0 @@
import bcrypt from 'bcryptjs'
import User from '../model/User'
export const updateUser = async (currentUsername: string, data: any) => {
const { displayName, username, password, isAdmin, isActive } = data
const params: any = { displayName, isAdmin, isActive }
if (username && currentUsername !== username) {
// Checking if username is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) throw new Error('Username already exists.')
params.username = username
}
if (password) {
// Hash passwords
const salt = await bcrypt.genSalt(10)
params.password = await bcrypt.hash(password, salt)
}
const updatedUser = await User.findOneAndUpdate(
{ username: currentUsername },
params,
{ new: true }
)
return {
displayName: updatedUser.displayName,
username: updatedUser.username,
isAdmin: updatedUser.isAdmin,
isActive: updatedUser.isActive
}
}

180
src/controllers/user.ts Normal file
View File

@@ -0,0 +1,180 @@
import {
Route,
Path,
Query,
Example,
Get,
Post,
Patch,
Delete,
Body,
Hidden
} from 'tsoa'
import bcrypt from 'bcryptjs'
import User, { UserPayload } from '../model/User'
interface userResponse {
username: string
displayName: string
}
interface userDetailsResponse {
displayName: string
username: string
isActive: boolean
isAdmin: boolean
}
@Route('user')
export default class UserController {
/**
* Get list of all users (username, displayname). All users can request this.
*
*/
@Example<userResponse[]>([
{
username: 'johnusername',
displayName: 'John'
},
{
username: 'starkusername',
displayName: 'Stark'
}
])
@Get('/')
public async getAllUsers(): Promise<userResponse[]> {
return getAllUsers()
}
/**
* Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.
*
*/
@Example<userDetailsResponse>({
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
isActive: true
})
@Post('/')
public async createUser(
@Body() body: UserPayload
): Promise<userDetailsResponse> {
return createUser(body)
}
/**
* Update user properties - such as displayName. Can be performed either by admins, or the user in question.
* @param username The user's identifier
* @example username "johnSnow01"
*/
@Example<userDetailsResponse>({
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
isActive: true
})
@Patch('{username}')
public async updateUser(
@Path() username: string,
@Body() body: UserPayload
): Promise<userDetailsResponse> {
return updateUser(username, body)
}
/**
* Delete a user. Can be performed either by admins, or the user in question.
* @param username The user's identifier
* @example username "johnSnow01"
*/
@Delete('{username}')
public async deleteUser(
@Path() username: string,
@Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false
) {
return deleteUser(username, isAdmin, body)
}
}
const getAllUsers = async () =>
await User.find({}).select({ _id: 0, username: 1, displayName: 1 }).exec()
const createUser = async (data: any): Promise<userDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive } = data
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) throw new Error('Username already exists.')
// Hash passwords
const salt = await bcrypt.genSalt(10)
const hashPassword = await bcrypt.hash(password, salt)
// Create a new user
const user = new User({
displayName,
username,
password: hashPassword,
isAdmin,
isActive
})
const savedUser = await user.save()
return {
displayName: savedUser.displayName,
username: savedUser.username,
isAdmin: savedUser.isAdmin,
isActive: savedUser.isActive
}
}
const updateUser = async (currentUsername: string, data: any) => {
const { displayName, username, password, isAdmin, isActive } = data
const params: any = { displayName, isAdmin, isActive }
if (username && currentUsername !== username) {
// Checking if username is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) throw new Error('Username already exists.')
params.username = username
}
if (password) {
// Hash passwords
const salt = await bcrypt.genSalt(10)
params.password = await bcrypt.hash(password, salt)
}
const updatedUser = await User.findOneAndUpdate(
{ username: currentUsername },
params,
{ new: true }
)
if (!updatedUser) throw new Error('Unable to update user')
return {
displayName: updatedUser.displayName,
username: updatedUser.username,
isAdmin: updatedUser.isAdmin,
isActive: updatedUser.isActive
}
}
const deleteUser = async (username: string, isAdmin: boolean, data: any) => {
const { password } = data
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
if (!isAdmin) {
const validPass = await bcrypt.compare(password, user.password)
if (!validPass) throw new Error('Invalid password.')
}
await User.deleteOne({ username })
}

View File

@@ -1,2 +1,3 @@
export * from './authenticateToken'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'

View File

@@ -0,0 +1,9 @@
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => {
const { user } = req
const { username } = req.params
if (!user.isAdmin && user.username !== username) {
return res.status(401).send('Admin account required')
}
next()
}

View File

@@ -1,42 +1,76 @@
import mongoose from 'mongoose'
import { Schema, model } from 'mongoose'
const userSchema = new mongoose.Schema({
displayName: {
type: String,
required: true
},
username: {
type: String,
required: true
},
password: {
type: String,
required: true
},
isAdmin: {
type: Boolean,
default: false
},
isActive: {
type: Boolean,
default: true
},
tokens: [
{
clientId: {
type: String,
required: true
},
accessToken: {
type: String,
required: true
},
refreshToken: {
type: String,
required: true
export interface UserPayload {
/**
* Display name for user
* @example "John Snow"
*/
displayName: string
/**
* Username for user
* @example "johnSnow01"
*/
username: string
/**
* Password for user
*/
password: string
/**
* Account should be admin or not, defaults to false
* @example "false"
*/
isAdmin?: boolean
/**
* Account should be active or not, defaults to true
* @example "true"
*/
isActive?: boolean
}
interface UserSchema extends UserPayload {
isAdmin: boolean
isActive: boolean
tokens: [{ [key: string]: string }]
}
export default model(
'User',
new Schema<UserSchema>({
displayName: {
type: String,
required: true
},
username: {
type: String,
required: true
},
password: {
type: String,
required: true
},
isAdmin: {
type: Boolean,
default: false
},
isActive: {
type: Boolean,
default: true
},
tokens: [
{
clientId: {
type: String,
required: true
},
accessToken: {
type: String,
required: true
},
refreshToken: {
type: String,
required: true
}
}
}
]
})
export default mongoose.model('User', userSchema)
]
})
)

View File

@@ -1,5 +1,6 @@
import express from 'express'
import dotenv from 'dotenv'
import swaggerUi from 'swagger-ui-express'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
@@ -19,5 +20,14 @@ router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/user', userRouter)
router.use('/client', authenticateAccessToken, verifyAdmin, clientRouter)
router.use('/auth', authRouter)
router.use(
'/',
swaggerUi.serve,
swaggerUi.setup(undefined, {
swaggerOptions: {
url: '/swagger.json'
}
})
)
export default router

View File

@@ -2,7 +2,7 @@ import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import app from '../../../app'
import { createUser } from '../../../controllers/createUser'
import UserController from '../../../controllers/user'
import { createClient } from '../../../controllers/createClient'
import {
generateAccessToken,
@@ -27,6 +27,7 @@ const user = {
describe('auth', () => {
let con: Mongoose
let mongoServer: MongoMemoryServer
const userController = new UserController()
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create()
@@ -49,7 +50,7 @@ describe('auth', () => {
})
it('should respond with authorization code', async () => {
await createUser(user)
await userController.createUser(user)
const res = await request(app)
.post('/SASjsApi/auth/authorize')
@@ -117,7 +118,7 @@ describe('auth', () => {
})
it('should respond with Forbidden if password is incorrect', async () => {
await createUser(user)
await userController.createUser(user)
const res = await request(app)
.post('/SASjsApi/auth/authorize')
@@ -133,7 +134,7 @@ describe('auth', () => {
})
it('should respond with Forbidden if clientId is incorrect', async () => {
await createUser(user)
await userController.createUser(user)
const res = await request(app)
.post('/SASjsApi/auth/authorize')
@@ -155,7 +156,7 @@ describe('auth', () => {
username: user.username
}
beforeAll(async () => {
await createUser(user)
await userController.createUser(user)
})
afterAll(async () => {
const collections = mongoose.connection.collections
@@ -250,7 +251,7 @@ describe('auth', () => {
})
beforeEach(async () => {
await createUser(user)
await userController.createUser(user)
await saveTokensInDB(user.username, clientId, 'accessToken', refreshToken)
})
@@ -294,7 +295,7 @@ describe('auth', () => {
})
beforeEach(async () => {
await createUser(user)
await userController.createUser(user)
await saveTokensInDB(user.username, clientId, accessToken, 'refreshToken')
})

View File

@@ -3,8 +3,8 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import app from '../../../app'
import { createClient } from '../../../controllers/createClient'
import UserController from '../../../controllers/user'
import { generateAccessToken } from '../auth'
import { createUser } from '../../../controllers/createUser'
import { saveTokensInDB } from '../../../utils'
const client = {
@@ -23,9 +23,10 @@ const newClient = {
clientSecret: 'newClientSecret'
}
describe('user', () => {
describe('client', () => {
let con: Mongoose
let mongoServer: MongoMemoryServer
const userController = new UserController()
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create()
@@ -45,7 +46,7 @@ describe('user', () => {
})
beforeAll(async () => {
await createUser(adminUser)
await userController.createUser(adminUser)
await saveTokensInDB(
adminUser.username,
client.clientId,
@@ -93,7 +94,7 @@ describe('user', () => {
clientId: client.clientId,
username: user.username
})
await createUser(user)
await userController.createUser(user)
await saveTokensInDB(
user.username,
client.clientId,
@@ -105,7 +106,7 @@ describe('user', () => {
.post('/SASjsApi/client')
.auth(accessToken, { type: 'bearer' })
.send(newClient)
.expect(403)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})

View File

@@ -3,11 +3,11 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import app from '../../../app'
import { getTreeExample } from '../../../controllers/deploy'
import UserController from '../../../controllers/user'
import { getTmpFilesFolderPath } from '../../../utils/file'
import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils'
import path from 'path'
import { generateAccessToken } from '../auth'
import { createUser } from '../../../controllers/createUser'
import { saveTokensInDB } from '../../../utils'
const clientId = 'someclientID'
@@ -22,6 +22,7 @@ const user = {
describe('files', () => {
let con: Mongoose
let mongoServer: MongoMemoryServer
const controller = new UserController()
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create()
@@ -40,7 +41,7 @@ describe('files', () => {
})
beforeAll(async () => {
await createUser(user)
await controller.createUser(user)
await saveTokensInDB(user.username, clientId, accessToken, 'refreshToken')
})
const shouldFailAssertion = async (payload: any) => {

View File

@@ -2,7 +2,7 @@ import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import app from '../../../app'
import { createUser } from '../../../controllers/createUser'
import UserController from '../../../controllers/user'
import { generateAccessToken } from '../auth'
import { saveTokensInDB } from '../../../utils'
@@ -25,6 +25,7 @@ const user = {
describe('user', () => {
let con: Mongoose
let mongoServer: MongoMemoryServer
const controller = new UserController()
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create()
@@ -44,7 +45,7 @@ describe('user', () => {
})
beforeEach(async () => {
await createUser(adminUser)
await controller.createUser(adminUser)
await saveTokensInDB(
adminUser.username,
clientId,
@@ -87,21 +88,21 @@ describe('user', () => {
clientId,
username: user.username
})
await createUser(user)
await controller.createUser(user)
await saveTokensInDB(user.username, clientId, accessToken, 'refreshToken')
const res = await request(app)
.post('/SASjsApi/user')
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(403)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
await createUser(user)
await controller.createUser(user)
const res = await request(app)
.post('/SASjsApi/user')

View File

@@ -1,8 +1,10 @@
import express from 'express'
import { createUser } from '../../controllers/createUser'
import { updateUser } from '../../controllers/updateUser'
import { deleteUser } from '../../controllers/deleteUser'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
import UserController from '../../controllers/user'
import {
authenticateAccessToken,
verifyAdmin,
verifyAdminIfNeeded
} from '../../middlewares'
import User from '../../model/User'
import {
deleteUserValidation,
@@ -12,29 +14,31 @@ import {
const userRouter = express.Router()
// create user
userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
const { error, value: data } = registerUserValidation(req.body)
const { error, value: body } = registerUserValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
const savedUser = await createUser(data)
res.send(savedUser)
const response = await controller.createUser(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
userRouter.get('/', authenticateAccessToken, async (req, res) => {
const controller = new UserController()
try {
const users = await User.find({})
.select({ _id: 0, username: 1, displayName: 1, isAdmin: 1, isActive: 1 })
.exec()
res.send(users)
const response = await controller.getAllUsers()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
// get one user
userRouter.get('/:username', authenticateAccessToken, async (req: any, res) => {
const { username } = req.params
try {
@@ -47,48 +51,45 @@ userRouter.get('/:username', authenticateAccessToken, async (req: any, res) => {
}
})
// update user
userRouter.patch(
'/:username',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req: any, res) => {
const { user } = req
const { username } = req.params
// only an admin can update other users
if (!user.isAdmin && user.username !== username) {
return res.status(401).send('Admin account required')
}
// only an admin can update `isActive` and `isAdmin` fields
const { error, value: data } = updateUserValidation(req.body, user.isAdmin)
const { error, value: body } = updateUserValidation(req.body, user.isAdmin)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
const user = await updateUser(username, data)
res.send(user)
const response = await controller.updateUser(username, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
// delete user
userRouter.delete(
'/:username',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req: any, res) => {
const { user } = req
const { username } = req.params
// only an admin can delete other users
if (!user.isAdmin && user.username !== username) {
return res.status(401).send('Admin account required')
}
// only an admin can delete user without providing password
const { error, value: data } = deleteUserValidation(req.body, user.isAdmin)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
await deleteUser(username, user.isAdmin, data)
await controller.deleteUser(username, data, user.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())

View File

@@ -2,6 +2,7 @@ import User from '../model/User'
export const removeTokensInDB = async (username: string, clientId: string) => {
const user = await User.findOne({ username })
if (!user) return
const tokenObjIndex = user.tokens.findIndex(
(tokenObj: any) => tokenObj.clientId === clientId

View File

@@ -7,6 +7,7 @@ export const saveTokensInDB = async (
refreshToken: string
) => {
const user = await User.findOne({ username })
if (!user) return
const currentTokenObj = user.tokens.find(
(tokenObj: any) => tokenObj.clientId === clientId

View File

@@ -6,7 +6,9 @@
"outDir": "./build",
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"ts-node": {
"files": true

8
tsoa.json Normal file
View File

@@ -0,0 +1,8 @@
{
"entryFile": "src/app.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"spec": {
"outputDirectory": "public",
"specVersion": 3
}
}