diff --git a/striker-ui-api/src/lib/request_handlers/user/createUser.ts b/striker-ui-api/src/lib/request_handlers/user/createUser.ts new file mode 100644 index 00000000..29f9db05 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/user/createUser.ts @@ -0,0 +1,64 @@ +import assert from 'assert'; +import { RequestHandler } from 'express'; + +import { REP_PEACEFUL_STRING, REP_UUID } from '../../consts'; + +import { insertOrUpdateUser, query } from '../../accessModule'; +import { sanitize } from '../../sanitize'; +import { openssl, stderr, stdoutVar } from '../../shell'; + +export const createUser: RequestHandler< + unknown, + CreateUserResponseBody, + CreateUserRequestBody +> = async (request, response) => { + const { body: { password: rPassword, userName: rUserName } = {} } = request; + + const password = sanitize(rPassword, 'string', { + fallback: openssl('rand', '-base64', '12').trim(), + }); + const userName = sanitize(rUserName, 'string', { modifierType: 'sql' }); + + stdoutVar({ password, userName }, 'Create user with params: '); + + try { + assert( + REP_PEACEFUL_STRING.test(password), + `Password must be a peaceful string; got: [${password}]`, + ); + + assert( + REP_PEACEFUL_STRING.test(userName), + `User name must be a peaceful string; got: [${userName}]`, + ); + + const [[userCount]]: [[number]] = await query( + `SELECT COUNT(user_uuid) FROM users WHERE user_name = '${userName}';`, + ); + + assert(userCount === 0, `User name [${userName}] already used`); + } catch (error) { + stderr(`Failed to assert value when creating user; CAUSE: ${error}`); + + return response.status(400).send(); + } + + try { + const result = await insertOrUpdateUser({ + file: __filename, + user_name: userName, + user_password_hash: password, + }); + + assert( + REP_UUID.test(result), + `Insert or update failed with result [${result}]`, + ); + } catch (error) { + stderr(`Failed to record user to database; CAUSE: ${error}`); + + return response.status(500).send(); + } + + return response.status(201).send({ password }); +}; diff --git a/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts b/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts index 2170ba3c..693d4d71 100644 --- a/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts +++ b/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts @@ -9,7 +9,7 @@ import { sanitize } from '../../sanitize'; import { stderr, stdoutVar } from '../../shell'; export const deleteUser: RequestHandler< - DeleteUserParamsDictionary, + UserParamsDictionary, undefined, DeleteUserRequestBody > = async (request, response) => { @@ -45,7 +45,10 @@ export const deleteUser: RequestHandler< const wcode = await write( `UPDATE users SET user_algorithm = '${DELETED}' - WHERE user_uuid IN (${join(ulist)});`, + WHERE user_uuid IN (${join(ulist, { + elementWrapper: "'", + separator: ',', + })});`, ); assert(wcode === 0, `Write exited with code ${wcode}`); diff --git a/striker-ui-api/src/lib/request_handlers/user/index.ts b/striker-ui-api/src/lib/request_handlers/user/index.ts index 2649ee0e..4e094a9c 100644 --- a/striker-ui-api/src/lib/request_handlers/user/index.ts +++ b/striker-ui-api/src/lib/request_handlers/user/index.ts @@ -1,2 +1,4 @@ +export * from './createUser'; export * from './deleteUser'; export * from './getUser'; +export * from './updateUser'; diff --git a/striker-ui-api/src/lib/request_handlers/user/updateUser.ts b/striker-ui-api/src/lib/request_handlers/user/updateUser.ts new file mode 100644 index 00000000..b870b87d --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/user/updateUser.ts @@ -0,0 +1,136 @@ +import assert from 'assert'; +import { RequestHandler } from 'express'; + +import { REP_PEACEFUL_STRING, REP_UUID } from '../../consts'; + +import { encrypt, query, write } from '../../accessModule'; +import { sanitize } from '../../sanitize'; +import { stderr, stdoutVar } from '../../shell'; + +export const updateUser: RequestHandler< + UserParamsDictionary, + undefined, + UpdateUserRequestBody +> = async (request, response) => { + const { + body: { password: rPassword, userName: rUserName } = {}, + params: { userUuid }, + } = request; + + const password = sanitize(rPassword, 'string'); + const userName = sanitize(rUserName, 'string', { modifierType: 'sql' }); + + stdoutVar({ password, userName }, `Update user ${userUuid} with params: `); + + try { + if (password.length) { + assert( + REP_PEACEFUL_STRING.test(password), + `Password must be a valid peaceful string; got: [${password}]`, + ); + } + + if (userName.length) { + assert( + REP_PEACEFUL_STRING.test(userName), + `User name must be a peaceful string; got: [${userName}]`, + ); + } + + assert( + REP_UUID.test(userUuid), + `User UUID must be a valid UUIDv4; got: [${userUuid}]`, + ); + + const [[existingUserName]]: [[string]] = await query( + `SELECT user_name FROM users WHERE user_uuid = '${userUuid}';`, + ); + + assert(existingUserName !== 'admin' || userName, 'Cannot '); + } catch (error) { + stderr(`Assert failed when update user; CAUSE: ${error}`); + + return response.status(400).send(); + } + + let existingUser: [ + [ + user_name: string, + user_password_hash: string, + user_salt: string, + user_algorithm: string, + user_hash_count: string, + ], + ]; + + try { + existingUser = await query( + `SELECT + user_name, + user_password_hash, + user_salt, + user_algorithm, + user_hash_count + FROM users + WHERE user_uuid = '${userUuid}' + ORDER BY modified_date DESC + LIMIT 1;`, + ); + } catch (error) { + stderr(`Failed to find existing user ${userUuid}; CAUSE: ${error}`); + + return response.status(500).send(); + } + + if (existingUser.length !== 1) { + return response.status(404).send(); + } + + const [[xUserName, xPasswordHash, xSalt, xAlgorithm, xHashCount]] = + existingUser; + + const assigns: string[] = []; + + if (password.length) { + let passwordHash: string; + + try { + ({ user_password_hash: passwordHash } = await encrypt({ + algorithm: xAlgorithm, + hash_count: xHashCount, + password, + salt: xSalt, + })); + } catch (error) { + stderr(`Encrypt failed when update user; CAUSE ${error}`); + + return response.status(500).send(); + } + + if (passwordHash !== xPasswordHash) { + assigns.push(`user_password_hash = '${passwordHash}'`); + } + } + + if (userName.length && userName !== xUserName) { + assigns.push(`user_name = '${userName}'`); + } + + if (assigns.length) { + try { + const wcode = await write( + `UPDATE users SET ${assigns.join( + ',', + )} WHERE user_uuid = '${userUuid}';`, + ); + + assert(wcode === 0, `Update users failed with code: ${wcode}`); + } catch (error) { + stderr(`Failed to record user changes to database; CAUSE: ${error}`); + + return response.status(500).send(); + } + } + + return response.send(); +}; diff --git a/striker-ui-api/src/routes/user.ts b/striker-ui-api/src/routes/user.ts index 4cc94a06..7a8f8c10 100644 --- a/striker-ui-api/src/routes/user.ts +++ b/striker-ui-api/src/routes/user.ts @@ -1,11 +1,18 @@ import express from 'express'; -import { deleteUser, getUser } from '../lib/request_handlers/user'; +import { + createUser, + deleteUser, + getUser, + updateUser, +} from '../lib/request_handlers/user'; const router = express.Router(); router .get('/', getUser) + .post('/', createUser) + .put('/:userUuid', updateUser) .delete('/', deleteUser) .delete('/:userUuid', deleteUser); diff --git a/striker-ui-api/src/types/ApiUser.d.ts b/striker-ui-api/src/types/ApiUser.d.ts index 213a6871..577d5d28 100644 --- a/striker-ui-api/src/types/ApiUser.d.ts +++ b/striker-ui-api/src/types/ApiUser.d.ts @@ -1,7 +1,18 @@ -type DeleteUserParamsDictionary = { - userUuid: string; +type CreateUserRequestBody = { + password?: string; + userName: string; +}; + +type CreateUserResponseBody = { + password: string; }; type DeleteUserRequestBody = { uuids?: string[]; }; + +type UpdateUserRequestBody = Partial; + +type UserParamsDictionary = { + userUuid: string; +};