1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-07 06:30:06 +00:00

chore(merge): Merge branch 'master' into authentication-with-jwt

This commit is contained in:
Saad Jutt
2021-11-09 18:24:35 +05:00
83 changed files with 43080 additions and 10960 deletions

View File

@@ -0,0 +1,80 @@
import { Security, Route, Tags, Example, Post, Body, Response } from 'tsoa'
import { fileExists, readFile, createFile } from '@sasjs/utils'
import { createFileTree, getTreeExample } from '.'
import { FileTree, isFileTree } from '../types'
interface DeployPayload {
appLoc?: string
fileTree: FileTree
}
interface DeployResponse {
status: string
message: string
example?: FileTree
}
const fileTreeExample = getTreeExample()
const successResponse: DeployResponse = {
status: 'success',
message: 'Files deployed successfully to @sasjs/server.'
}
const invalidFormatResponse: DeployResponse = {
status: 'failure',
message: 'Provided not supported data format.',
example: fileTreeExample
}
const execErrorResponse: DeployResponse = {
status: 'failure',
message: 'Deployment failed!'
}
@Security('bearerAuth')
@Route('SASjsApi/drive')
@Tags('Drive')
export class DriveController {
/**
* Creates/updates files within SASjs Drive using provided payload.
*
*/
@Example<DeployResponse>(successResponse)
@Response<DeployResponse>(400, 'Invalid Format', invalidFormatResponse)
@Response<DeployResponse>(500, 'Execution Error', execErrorResponse)
@Post('/deploy')
public async deploy(@Body() body: DeployPayload): Promise<DeployResponse> {
return deploy(body)
}
async readFile(filePath: string) {
await this.validateFilePath(filePath)
return await readFile(filePath)
}
async updateFile(filePath: string, fileContent: string) {
await this.validateFilePath(filePath)
return await createFile(filePath, fileContent)
}
private async validateFilePath(filePath: string) {
if (!(await fileExists(filePath))) {
throw 'DriveController: File does not exists.'
}
}
}
const deploy = async (data: DeployPayload) => {
if (!isFileTree(data.fileTree)) {
throw { code: 400, ...invalidFormatResponse }
}
await createFileTree(
data.fileTree.members,
data.appLoc ? data.appLoc.replace(/^\//, '').split('/') : []
).catch((err) => {
throw { code: 500, ...execErrorResponse, ...err }
})
return successResponse
}

View File

@@ -0,0 +1,151 @@
import path from 'path'
import fs from 'fs'
import { getSessionController } from './'
import { readFile, fileExists, createFile } from '@sasjs/utils'
import { configuration } from '../../package.json'
import { promisify } from 'util'
import { execFile } from 'child_process'
import { Session, TreeNode } from '../types'
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../utils'
const execFilePromise = promisify(execFile)
export class ExecutionController {
async execute(
program = '',
autoExec?: string,
session?: Session,
vars?: any,
otherArgs?: any,
returnJson?: boolean
) {
if (program) {
if (!(await fileExists(program))) {
throw 'ExecutionController: SAS file does not exist.'
}
program = await readFile(program)
if (vars) {
Object.keys(vars).forEach(
(key: string) => (program = `%let ${key}=${vars[key]};\n${program}`)
)
}
}
const sessionController = getSessionController()
if (!session) {
session = await sessionController.getSession()
session.inUse = true
}
let log = path.join(session.path, 'log.log')
let webout = path.join(session.path, 'webout.txt')
await createFile(webout, '')
program = `
%let sasjsprocessmode=Stored Program;
filename _webout "${webout}";
${program}`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs && otherArgs.filesNamesMap) {
const uploadSasCode = await generateFileUploadSasCode(
otherArgs.filesNamesMap,
session.path
)
//If sas code for the file is generated it will be appended to the top of sasCode
if (uploadSasCode.length > 0) {
program = `${uploadSasCode}` + program
}
}
const code = path.join(session.path, 'code.sas')
if (!(await fileExists(code))) {
await createFile(code, program)
}
let additionalArgs: string[] = []
if (autoExec) additionalArgs = ['-AUTOEXEC', autoExec]
const { stdout, stderr } = await execFilePromise(configuration.sasPath, [
'-SYSIN',
code,
'-LOG',
log,
'-WORK',
session.path,
...additionalArgs,
process.platform === 'win32' ? '-nosplash' : ''
]).catch((err) => ({ stderr: err, stdout: '' }))
if (await fileExists(log)) log = await readFile(log)
else log = ''
if (await fileExists(webout)) webout = await readFile(webout)
else webout = ''
const debug = Object.keys(vars).find(
(key: string) => key.toLowerCase() === '_debug'
)
let jsonResult
if ((debug && vars[debug] >= 131) || stderr) {
webout = `<html><body>
${webout}
<div style="text-align:left">
<hr /><h2>SAS Log</h2>
<pre>${log}</pre>
</div>
</body></html>`
} else if (returnJson) {
jsonResult = { result: webout, log: log }
}
session.inUse = false
sessionController.deleteSession(session)
return Promise.resolve(jsonResult || webout)
}
buildDirectorytree() {
const root: TreeNode = {
name: 'files',
relativePath: '',
absolutePath: getTmpFilesFolderPath(),
children: []
}
const stack = [root]
while (stack.length) {
const currentNode = stack.pop()
if (currentNode) {
const children = fs.readdirSync(currentNode.absolutePath)
for (let child of children) {
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
const relativeChildPath = `${currentNode.relativePath}/${child}`
const childNode: TreeNode = {
name: child,
relativePath: relativeChildPath,
absolutePath: absoluteChildPath,
children: []
}
currentNode.children.push(childNode)
if (fs.statSync(childNode.absolutePath).isDirectory()) {
stack.push(childNode)
}
}
}
}
return root
}
}

View File

@@ -0,0 +1,36 @@
import { uuidv4 } from '@sasjs/utils'
import { getSessionController } from '.'
const multer = require('multer')
export class FileUploadController {
private storage = multer.diskStorage({
destination: function (req: any, file: any, cb: any) {
//Sending the intercepted files to the sessions subfolder
cb(null, req.sasSession.path)
},
filename: function (req: any, file: any, cb: any) {
//req_file prefix + unique hash added to sas request files
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
}
})
private upload = multer({ storage: this.storage })
//It will intercept request and generate unique uuid to be used as a subfolder name
//that will store the files uploaded
public preuploadMiddleware = async (req: any, res: any, next: any) => {
let session
const sessionController = getSessionController()
session = await sessionController.getSession()
session.inUse = true
req.sasSession = session
next()
}
public getMulterUploadObject() {
return this.upload
}
}

View File

@@ -0,0 +1,127 @@
import { Session } from '../types'
import { getTmpSessionsFolderPath, generateUniqueFileName } from '../utils'
import {
deleteFolder,
createFile,
fileExists,
deleteFile,
generateTimestamp
} from '@sasjs/utils'
import path from 'path'
import { ExecutionController } from './Execution'
export class SessionController {
private sessions: Session[] = []
private executionController: ExecutionController
constructor() {
this.executionController = new ExecutionController()
}
public async getSession() {
const readySessions = this.sessions.filter((sess: Session) => sess.ready)
const session = readySessions.length
? readySessions[0]
: await this.createSession()
if (readySessions.length < 2) this.createSession()
return session
}
private async createSession() {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(await getTmpSessionsFolderPath(), sessionId)
const autoExecContent = `data _null_;
/* remove the dummy SYSIN */
length fname $8;
rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname);
/* now wait for the real SYSIN */
slept=0;
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
slept=slept+sleep(0.01,1);
end;
run;
EOL`
const autoExec = path.join(sessionFolder, 'autoexec.sas')
await createFile(autoExec, autoExecContent)
await createFile(path.join(sessionFolder, 'code.sas'), '')
const creationTimeStamp = sessionId.split('-').pop() as string
const session: Session = {
id: sessionId,
ready: false,
creationTimeStamp: creationTimeStamp,
deathTimeStamp: (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString(),
path: sessionFolder,
inUse: false
}
this.scheduleSessionDestroy(session)
this.executionController.execute('', autoExec, session).catch(() => {})
this.sessions.push(session)
await this.waitForSession(session)
return session
}
public async waitForSession(session: Session) {
if (await fileExists(path.join(session.path, 'code.sas'))) {
while (await fileExists(path.join(session.path, 'code.sas'))) {}
await deleteFile(path.join(session.path, 'log.log'))
session.ready = true
return Promise.resolve(session)
} else {
session.ready = true
return Promise.resolve(session)
}
}
public async deleteSession(session: Session) {
await deleteFolder(session.path)
if (session.ready) {
this.sessions = this.sessions.filter(
(sess: Session) => sess.id !== session.id
)
}
}
private scheduleSessionDestroy(session: Session) {
setTimeout(async () => {
if (session.inUse) {
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
}
}
export const getSessionController = () => {
if (process.sessionController) return process.sessionController
process.sessionController = new SessionController()
return process.sessionController
}

221
api/src/controllers/auth.ts Normal file
View File

@@ -0,0 +1,221 @@
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
import jwt from 'jsonwebtoken'
import User from '../model/User'
import { InfoJWT } from '../types'
import { removeTokensInDB, saveTokensInDB } from '../utils'
@Route('SASjsApi/auth')
@Tags('Auth')
export default class AuthController {
static authCodes: { [key: string]: { [key: string]: string } } = {}
static saveCode = (userId: number, clientId: string, code: string) => {
if (AuthController.authCodes[userId])
return (AuthController.authCodes[userId][clientId] = code)
AuthController.authCodes[userId] = { [clientId]: code }
return AuthController.authCodes[userId][clientId]
}
static deleteCode = (userId: number, clientId: string) =>
delete AuthController.authCodes[userId][clientId]
/**
* 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)
}
/**
* Accepts client/auth code and returns access/refresh tokens
*
*/
@Example<TokenResponse>({
accessToken: 'someRandomCryptoString',
refreshToken: 'someRandomCryptoString'
})
@Post('/token')
public async token(@Body() body: TokenPayload): Promise<TokenResponse> {
return token(body)
}
/**
* Returns new access/refresh tokens
*
*/
@Example<TokenResponse>({
accessToken: 'someRandomCryptoString',
refreshToken: 'someRandomCryptoString'
})
@Security('bearerAuth')
@Post('/refresh')
public async refresh(
@Query() @Hidden() data?: InfoJWT
): Promise<TokenResponse> {
return refresh(data!)
}
/**
* Logout terminate access/refresh tokens and returns nothing
*
*/
@Security('bearerAuth')
@Post('/logout')
public async logout(@Query() @Hidden() data?: InfoJWT) {
return logout(data!)
}
}
const authorize = async (data: any): Promise<AuthorizeResponse> => {
const { username, password, clientId } = data
// 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 { clientId, code } = data
const userInfo = await verifyAuthCode(clientId, code)
if (!userInfo) throw new Error('Invalid Auth Code')
if (AuthController.authCodes[userInfo.userId][clientId] !== code)
throw new Error('Invalid Auth Code')
AuthController.deleteCode(userInfo.userId, clientId)
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
return { accessToken, refreshToken }
}
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
await saveTokensInDB(
userInfo.userId,
userInfo.clientId,
accessToken,
refreshToken
)
return { accessToken, refreshToken }
}
const logout = async (userInfo: InfoJWT) => {
await removeTokensInDB(userInfo.userId, userInfo.clientId)
}
interface AuthorizePayload {
/**
* Username for user
* @example "secretuser"
*/
username: string
/**
* Password for user
* @example "secretpassword"
*/
password: string
/**
* Client ID
* @example "clientID1"
*/
clientId: string
}
interface AuthorizeResponse {
/**
* Authorization code
* @example "someRandomCryptoString"
*/
code: string
}
interface TokenPayload {
/**
* Client ID
* @example "clientID1"
*/
clientId: string
/**
* Authorization code
* @example "someRandomCryptoString"
*/
code: string
}
interface TokenResponse {
/**
* Access Token
* @example "someRandomCryptoString"
*/
accessToken: string
/**
* Refresh Token
* @example "someRandomCryptoString"
*/
refreshToken: string
}
export const generateAuthCode = (data: InfoJWT) =>
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, {
expiresIn: '30s'
})
export const generateAccessToken = (data: InfoJWT) =>
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
expiresIn: '1h'
})
export const generateRefreshToken = (data: InfoJWT) =>
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
expiresIn: '1day'
})
const verifyAuthCode = async (
clientId: string,
code: string
): Promise<InfoJWT | undefined> => {
return new Promise((resolve, reject) => {
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
if (err) return resolve(undefined)
const clientInfo: InfoJWT = {
clientId: data?.clientId,
userId: data?.userId
}
if (clientInfo.clientId === clientId) {
return resolve(clientInfo)
}
return resolve(undefined)
})
})
}

View File

@@ -0,0 +1,44 @@
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
import Client, { ClientPayload } from '../model/Client'
@Security('bearerAuth')
@Route('SASjsApi/client')
@Tags('Client')
export default class ClientController {
/**
* Create client with the following attributes: ClientId, ClientSecret. Admin only task.
*
*/
@Example<ClientPayload>({
clientId: 'someFormattedClientID1234',
clientSecret: 'someRandomCryptoString'
})
@Post('/')
public async createClient(
@Body() body: ClientPayload
): Promise<ClientPayload> {
return createClient(body)
}
}
const createClient = async (data: any): Promise<ClientPayload> => {
const { clientId, clientSecret } = data
// Checking if client is already in the database
const clientExist = await Client.findOne({ clientId })
if (clientExist) throw new Error('Client ID already exists.')
// Create a new client
const client = new Client({
clientId,
clientSecret
})
const savedClient = await client.save()
return {
clientId: savedClient.clientId,
clientSecret: savedClient.clientSecret
}
}

View File

@@ -0,0 +1,59 @@
import { MemberType, FolderMember, ServiceMember, FileTree } from '../types'
import { getTmpFilesFolderPath } from '../utils/file'
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
import path from 'path'
// REFACTOR: export FileTreeCpntroller
export const createFileTree = async (
members: (FolderMember | ServiceMember)[],
parentFolders: string[] = []
) => {
const destinationPath = path.join(
getTmpFilesFolderPath(),
path.join(...parentFolders)
)
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
let name = member.name
if (member.type === MemberType.service) name += '.sas'
if (member.type === MemberType.folder) {
await createFolder(path.join(destinationPath, name)).catch((err) =>
Promise.reject({ error: err, failedToCreate: name })
)
await createFileTree(member.members, [...parentFolders, name]).catch(
(err) => Promise.reject({ error: err, failedToCreate: name })
)
} else {
await createFile(path.join(destinationPath, name), member.code).catch(
(err) => Promise.reject({ error: err, failedToCreate: name })
)
}
})
return Promise.resolve()
}
export const getTreeExample = (): FileTree => ({
members: [
{
name: 'jobs',
type: MemberType.folder,
members: [
{
name: 'extract',
type: MemberType.folder,
members: [
{
name: 'makedata1',
type: MemberType.service,
code: '%put Hello World!;'
}
]
}
]
}
]
})

View File

@@ -0,0 +1,230 @@
import {
Security,
Route,
Tags,
Path,
Example,
Get,
Post,
Delete,
Body
} from 'tsoa'
import Group, { GroupPayload } from '../model/Group'
import User from '../model/User'
import { UserResponse } from './user'
interface GroupResponse {
groupId: number
name: string
description: string
}
interface GroupDetailsResponse {
groupId: number
name: string
description: string
isActive: boolean
users: UserResponse[]
}
@Security('bearerAuth')
@Route('SASjsApi/group')
@Tags('Group')
export default class GroupController {
/**
* Get list of all groups (groupName and groupDescription). All users can request this.
*
*/
@Example<GroupResponse[]>([
{
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users'
}
])
@Get('/')
public async getAllGroups(): Promise<GroupResponse[]> {
return getAllGroups()
}
/**
* Create a new group. Admin only.
*
*/
@Example<GroupDetailsResponse>({
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
users: []
})
@Post('/')
public async createGroup(
@Body() body: GroupPayload
): Promise<GroupDetailsResponse> {
return createGroup(body)
}
/**
* Get list of members of a group (userName). All users can request this.
* @param groupId The group's identifier
* @example groupId 1234
*/
@Get('{groupId}')
public async getGroup(
@Path() groupId: number
): Promise<GroupDetailsResponse> {
return getGroup(groupId)
}
/**
* Add a user to a group. Admin task only.
* @param groupId The group's identifier
* @example groupId "1234"
* @param userId The user's identifier
* @example userId "6789"
*/
@Example<GroupDetailsResponse>({
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
users: []
})
@Post('{groupId}/{userId}')
public async addUserToGroup(
@Path() groupId: number,
@Path() userId: number
): Promise<GroupDetailsResponse> {
return addUserToGroup(groupId, userId)
}
/**
* Remove a user to a group. Admin task only.
* @param groupId The group's identifier
* @example groupId "1234"
* @param userId The user's identifier
* @example userId "6789"
*/
@Example<GroupDetailsResponse>({
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
users: []
})
@Delete('{groupId}/{userId}')
public async removeUserFromGroup(
@Path() groupId: number,
@Path() userId: number
): Promise<GroupDetailsResponse> {
return removeUserFromGroup(groupId, userId)
}
/**
* Delete a group. Admin task only.
* @param groupId The group's identifier
* @example groupId 1234
*/
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
const { deletedCount } = await Group.deleteOne({ groupId })
if (deletedCount) return
throw new Error('No Group deleted!')
}
}
const getAllGroups = async (): Promise<GroupResponse[]> =>
await Group.find({})
.select({ _id: 0, groupId: 1, name: 1, description: 1 })
.exec()
const createGroup = async ({
name,
description,
isActive
}: GroupPayload): Promise<GroupDetailsResponse> => {
const group = new Group({
name,
description,
isActive
})
const savedGroup = await group.save()
return {
groupId: savedGroup.groupId,
name: savedGroup.name,
description: savedGroup.description,
isActive: savedGroup.isActive,
users: []
}
}
const getGroup = async (groupId: number): Promise<GroupDetailsResponse> => {
const group = (await Group.findOne(
{ groupId },
'groupId name description isActive users -_id'
).populate(
'users',
'id username displayName -_id'
)) as unknown as GroupDetailsResponse
if (!group) throw new Error('Group is not found.')
return {
groupId: group.groupId,
name: group.name,
description: group.description,
isActive: group.isActive,
users: group.users
}
}
const addUserToGroup = async (
groupId: number,
userId: number
): Promise<GroupDetailsResponse> => {
const group = await Group.findOne({ groupId })
if (!group) throw new Error('Group not found')
const user = await User.findOne({ id: userId })
if (!user) throw new Error('User not found')
const updatedGroup = (await group.addUser(
user._id
)) as unknown as GroupDetailsResponse
if (!updatedGroup) throw new Error('Unable to update group')
return {
groupId: updatedGroup.groupId,
name: updatedGroup.name,
description: updatedGroup.description,
isActive: updatedGroup.isActive,
users: updatedGroup.users
}
}
const removeUserFromGroup = async (
groupId: number,
userId: number
): Promise<GroupDetailsResponse> => {
const group = await Group.findOne({ groupId })
if (!group) throw new Error('Group not found')
const user = await User.findOne({ id: userId })
if (!user) throw new Error('User not found')
const updatedGroup = (await group.removeUser(
user._id
)) as unknown as GroupDetailsResponse
if (!updatedGroup) throw new Error('Unable to update group')
return {
groupId: updatedGroup.groupId,
name: updatedGroup.name,
description: updatedGroup.description,
isActive: updatedGroup.isActive,
users: updatedGroup.users
}
}

View File

@@ -0,0 +1,5 @@
export * from './deploy'
export * from './drive'
export * from './Session'
export * from './Execution'
export * from './FileUploadController'

220
api/src/controllers/user.ts Normal file
View File

@@ -0,0 +1,220 @@
import {
Security,
Route,
Tags,
Path,
Query,
Example,
Get,
Post,
Patch,
Delete,
Body,
Hidden
} from 'tsoa'
import User, { UserPayload } from '../model/User'
export interface UserResponse {
id: number
username: string
displayName: string
}
interface UserDetailsResponse {
id: number
displayName: string
username: string
isActive: boolean
isAdmin: boolean
}
@Security('bearerAuth')
@Route('SASjsApi/user')
@Tags('User')
export default class UserController {
/**
* Get list of all users (username, displayname). All users can request this.
*
*/
@Example<UserResponse[]>([
{
id: 123,
username: 'johnusername',
displayName: 'John'
},
{
id: 456,
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>({
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
isActive: true
})
@Post('/')
public async createUser(
@Body() body: UserPayload
): Promise<UserDetailsResponse> {
return createUser(body)
}
/**
* Get user properties - such as group memberships, userName, displayName.
* @param userId The user's identifier
* @example userId 1234
*/
@Get('{userId}')
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
return getUser(userId)
}
/**
* Update user properties - such as displayName. Can be performed either by admins, or the user in question.
* @param userId The user's identifier
* @example userId "1234"
*/
@Example<UserDetailsResponse>({
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
isActive: true
})
@Patch('{userId}')
public async updateUser(
@Path() userId: number,
@Body() body: UserPayload
): Promise<UserDetailsResponse> {
return updateUser(userId, body)
}
/**
* Delete a user. Can be performed either by admins, or the user in question.
* @param userId The user's identifier
* @example userId 1234
*/
@Delete('{userId}')
public async deleteUser(
@Path() userId: number,
@Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false
) {
return deleteUser(userId, isAdmin, body)
}
}
const getAllUsers = async (): Promise<UserResponse[]> =>
await User.find({})
.select({ _id: 0, id: 1, username: 1, displayName: 1 })
.exec()
const createUser = async (data: UserPayload): 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 hashPassword = User.hashPassword(password)
// Create a new user
const user = new User({
displayName,
username,
password: hashPassword,
isAdmin,
isActive
})
const savedUser = await user.save()
return {
id: savedUser.id,
displayName: savedUser.displayName,
username: savedUser.username,
isActive: savedUser.isActive,
isAdmin: savedUser.isAdmin
}
}
const getUser = async (id: number): Promise<UserDetailsResponse> => {
const user = await User.findOne({ id })
.select({
_id: 0,
id: 1,
username: 1,
displayName: 1,
isAdmin: 1,
isActive: 1
})
.exec()
if (!user) throw new Error('User is not found.')
return user
}
const updateUser = async (
id: number,
data: UserPayload
): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive } = data
const params: any = { displayName, isAdmin, isActive }
if (username) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist?.id != id) throw new Error('Username already exists.')
params.username = username
}
if (password) {
// Hash passwords
params.password = User.hashPassword(password)
}
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true })
.select({
_id: 0,
id: 1,
username: 1,
displayName: 1,
isAdmin: 1,
isActive: 1
})
.exec()
if (!updatedUser) throw new Error('Unable to update user')
return updatedUser
}
const deleteUser = async (
id: number,
isAdmin: boolean,
{ password }: { password?: string }
) => {
const user = await User.findOne({ id })
if (!user) throw new Error('User is not found.')
if (!isAdmin) {
const validPass = user.comparePassword(password!)
if (!validPass) throw new Error('Invalid password.')
}
await User.deleteOne({ id })
}