From 8c4fcdf772a74106042ce4a2e1e72db03fee3e6a Mon Sep 17 00:00:00 2001 From: Calvin Lu Date: Thu, 11 Jan 2024 19:43:00 -0800 Subject: [PATCH] Add POST /api/v1/users/sign-in route --- src/api/index.ts | 9 +++++ src/api/routes/users.ts | 53 +++++++++++++++++++++++++++ src/app.ts | 3 ++ src/models/Config.model.ts | 24 +++++++----- src/models/Session.model.ts | 41 ++++++++++----------- src/models/User.model.ts | 49 ++++++++----------------- src/tests/database/index.spec.ts | 2 +- src/tests/utils/updateBuilder.spec.ts | 50 +++++++++++++++++++++++++ src/utils/updateBuilder.ts | 31 ++++++++++++++++ 9 files changed, 197 insertions(+), 65 deletions(-) create mode 100644 src/api/index.ts create mode 100644 src/api/routes/users.ts create mode 100644 src/tests/utils/updateBuilder.spec.ts create mode 100644 src/utils/updateBuilder.ts diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..e10c93e --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,9 @@ +import express from "express" + +import usersRouter from "./routes/users"; + +const router = express.Router(); + +router.use("/users", usersRouter); + +export default router; \ No newline at end of file diff --git a/src/api/routes/users.ts b/src/api/routes/users.ts new file mode 100644 index 0000000..db36998 --- /dev/null +++ b/src/api/routes/users.ts @@ -0,0 +1,53 @@ +import express from "express"; +import { createSession } from "../../models/Session.model"; + +import User, { getUserByPassword, updateUser } from "../../models/User.model"; + + +const router = express.Router(); + +// Middleware to check if a password in passed in the body +router.use(async (req, res, next) => { + if (!req.body.password) { + return res.status(401).json({ + description: "Missing Password", + }); + } + + let user; + + try { + user = await getUserByPassword(req.body.password); + } catch (err) { + if (err instanceof RowNotFoundError) { + return res.status(403).json({ + description: err.message, + }); + } + + return res.sendStatus(500); + } + + res.locals.user = user; + return next(); +}); + +router.post("/sign-in", async (_req, res) => { + const user: User = res.locals.user; + + if (user.signed_in) { + return res.status(400).json({ + description: `${user.first_name} ${user.last_name} is already signed in!`, + }); + } + + await updateUser(user.user_id, { signed_in: true, last_signed_in: Date.now() }); + + return res.status(200).json({ + description: `Signed in as ${user.first_name} ${user.last_name}` + }); +}); + + + +export default router; \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 6fc1af2..907e6cc 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,10 +3,13 @@ require("dotenv").config() import express from "express"; import logger from "./utils/logger"; +import api from "./api"; const app = express(); +app.use("/api/v1", api); + app.listen(process.env.PORT, () => { logger.info(`Listening on port ${process.env.PORT}!`); }) \ No newline at end of file diff --git a/src/models/Config.model.ts b/src/models/Config.model.ts index 8ab202c..c8c6d46 100644 --- a/src/models/Config.model.ts +++ b/src/models/Config.model.ts @@ -1,23 +1,24 @@ -import { config } from "dotenv"; import { ResultSetHeader, RowDataPacket } from "mysql2"; import database from "../database"; -interface Config extends RowDataPacket { +interface Config { name: string; value: string; } -async function getAllConfigs(): Promise { +interface ConfigRowDataPacket extends Config, RowDataPacket {} + +async function getAllConfigs(): Promise { const sql = "SELECT * FROM `configs`"; - const [configs] = await database.query(sql); + const [configs] = await database.query(sql); return configs; } async function getConfigByName(name: string): Promise { const sql = "SELECT * FROM `configs` WHERE name = ?"; - const [configs] = await database.query(sql, [name]); + const [configs] = await database.query(sql, [name]); if (configs.length < 1) { throw new RowNotFoundError( @@ -30,14 +31,16 @@ async function getConfigByName(name: string): Promise { async function createConfigs( newConfigs: { name: string; value: string }[] -): Promise { +): Promise { const sql = "INSERT INTO `configs` (name, value) VALUES ? RETURNING name, value"; const params = newConfigs.map((newConfig) => [ newConfig.name, newConfig.value, ]); - const [configs] = await database.query(sql, [params]); + const [configs] = await database.query(sql, [ + params, + ]); return configs; } @@ -45,7 +48,10 @@ async function createConfigs( async function createConfig(name: string, value: string): Promise { const sql = "INSERT INTO `configs` (name, value) VALUES (?, ?) RETURNING name, value"; - const [configs] = await database.query(sql, [name, value]); + const [configs] = await database.query(sql, [ + name, + value, + ]); return configs[0]; } @@ -62,7 +68,7 @@ async function updateConfig(name: string, newValue: string): Promise { async function deleteConfig(name: string): Promise { const sql = "DELETE FROM `configs` WHERE name = ? RETURNING name, value"; - const [configs] = await database.query(sql, [name]); + const [configs] = await database.query(sql, [name]); return configs[0]; } diff --git a/src/models/Session.model.ts b/src/models/Session.model.ts index b96af1a..6c1cc9e 100644 --- a/src/models/Session.model.ts +++ b/src/models/Session.model.ts @@ -1,25 +1,29 @@ import { ResultSetHeader, RowDataPacket } from "mysql2"; + import database from "../database"; +import updateBuilder from "../utils/updateBuilder"; -interface Session extends RowDataPacket { +interface Session { session_id: number; user_id: number; - start_time: bigint; - end_time: bigint; + start_time: number; + end_time: number; amended: boolean; } -async function getAllSessions(): Promise { +interface SessionRowDataPacket extends Session, RowDataPacket {} + +async function getAllSessions(): Promise { const sql = "SELECT * FROM `sessions`"; - const [sessions] = await database.query(sql); + const [sessions] = await database.query(sql); return sessions; } -async function getSessionsByPassword(password: number): Promise { +async function getSessionsByPassword(password: number): Promise { const sql = "SELECT * FROM `sessions` WHERE user_id = (SELECT user_id from users WHERE password = ? )"; - const [sessions] = await database.query(sql, [password]); + const [sessions] = await database.query(sql, [password]); if (sessions.length < 1) { throw new RowNotFoundError(`Session not found in table: sessions`); @@ -30,9 +34,9 @@ async function getSessionsByPassword(password: number): Promise { async function createSession( password: number, - start_time: bigint, + start_time: number, amended: boolean, - end_time?: bigint + end_time?: number ): Promise { let sql: string; let params: any[]; @@ -48,21 +52,14 @@ async function createSession( } const [resHeader] = await database.query(sql, params); - return resHeader.affectedRows == 1; + return resHeader.affectedRows === 1; } -async function updateSession( - password: number, - end_time: bigint -): Promise { - const sql = - "UPDATE `sessions` SET end_time = ? WHERE user_id = (SELECT user_id from 'users' WHERE password = ?)"; - const [resHeader] = await database.query(sql, [ - end_time, - password, - ]); +async function updateSession(session_id: number, values: Partial): Promise { + const update = updateBuilder("sessions", values, { session_id }); + const [resHeader] = await database.query(update.query, update.params); - return resHeader.affectedRows == 1; + return resHeader.affectedRows === 1; } async function deleteSession(session_id: number): Promise { @@ -71,7 +68,7 @@ async function deleteSession(session_id: number): Promise { session_id, ]); - return resHeader.affectedRows == 1; + return resHeader.affectedRows === 1; } async function deleteSessionsByUserPassword( diff --git a/src/models/User.model.ts b/src/models/User.model.ts index f04f2be..5ff0097 100644 --- a/src/models/User.model.ts +++ b/src/models/User.model.ts @@ -1,26 +1,29 @@ import { ResultSetHeader, RowDataPacket } from "mysql2"; import database from "../database"; +import updateBuilder from "../utils/updateBuilder"; -interface User extends RowDataPacket { +interface User { user_id: number; first_name: string; last_name: string; password: number; signed_in: boolean; - last_signed_in: bigint; - total_time: bigint; + last_signed_in: number; + total_time: number; } -async function getAllUsers(): Promise { +interface UserRowDataPacket extends User, RowDataPacket {}; + +async function getAllUsers(): Promise { const sql = "SELECT * FROM `users`"; - const [users] = await database.query(sql); + const [users] = await database.query(sql); return users; } async function getUserByPassword(password: number): Promise { const sql = "SELECT * FROM `users` WHERE password = ?"; - const [users] = await database.query(sql, [password]); + const [users] = await database.query(sql, [password]); if (users.length < 1) { throw new RowNotFoundError("User not found in table: users"); @@ -36,7 +39,7 @@ async function createUser( ): Promise { const sql = "INSERT INTO `users` (first_name, last_name, password) VALUES (?, ?, ?); SELECT user_id, first_name, last_name FROM users where password = ?"; - const [users] = await database.query(sql, [ + const [users] = await database.query(sql, [ first_name, last_name, password, @@ -46,37 +49,18 @@ async function createUser( return users[0]; } -async function updateUserTotalTime(password: number): Promise { - const sql = - "UPDATE users SET total_time = (SELECT SUM(CASE WHEN end_time - start_time < 43200000 THEN end_time - start_time ELSE 0 END) FROM sessions WHERE user_id = (SELECT user_id FROM users WHERE password = ?)); SELECT user_id, first_name, last_name from users where password = ?"; - const [resHeader] = await database.query(sql, [ - password, - password, - ]); - - return resHeader.affectedRows == 1; -} - -async function updateUserLastSignedIn( - password: number, - newValue: bigint -): Promise { - const sql = - "UPDATE `users` SET last_signed_in = ? WHERE password = ?; SELECT user_id, first_name, last_name, last_signed_in FROM `users` where password = ?"; - const [resHeader] = await database.query(sql, [ - newValue, - password, - password, - ]); +async function updateUser(user_id: number, values: Partial): Promise { + const update = updateBuilder("users", values, { user_id }); + const [resHeader] = await database.query(update.query, update.params); - return resHeader.affectedRows == 1; + return resHeader.affectedRows === 1; } async function deleteUser(password: number): Promise { const sql = "DELETE FROM `users` WHERE password = ?"; const [resHeader] = await database.query(sql, [password]); - return resHeader.affectedRows == 1; + return resHeader.affectedRows === 1; } export default User; @@ -84,7 +68,6 @@ export { getAllUsers, getUserByPassword, createUser, - updateUserTotalTime, - updateUserLastSignedIn, + updateUser, deleteUser, }; diff --git a/src/tests/database/index.spec.ts b/src/tests/database/index.spec.ts index 5a5844d..c339f7c 100644 --- a/src/tests/database/index.spec.ts +++ b/src/tests/database/index.spec.ts @@ -8,7 +8,7 @@ import database from "../../database/"; describe("SQL Database Accessor", () => { - it("should be connected to database", async () => { + it("is connected to database", async () => { const response: any = await database.query("SHOW STATUS WHERE `variable_name` = 'Threads_running'") // See how many threads are currently running const threadsRunning = response[0][0]["Value"]; diff --git a/src/tests/utils/updateBuilder.spec.ts b/src/tests/utils/updateBuilder.spec.ts new file mode 100644 index 0000000..b1a54ab --- /dev/null +++ b/src/tests/utils/updateBuilder.spec.ts @@ -0,0 +1,50 @@ +import { expect } from "chai"; + +import updateBuilder from "../../utils/updateBuilder"; + + +describe("SQL Update Query Builder", () => { + it("returns full prepared statement given table, updates, and conditions", () => { + const table = "users"; + const updates = { + first_name: "fod", + last_name: "bart", + bool_test: true + }; + const conditions = { + user_id: 2, + password: "pwd", + }; + + const update = updateBuilder(table, updates, conditions); + + expect(update).to.be.deep.equal({ + query: "UPDATE ? SET ? WHERE ?", + params: [ + "users", + "first_name = 'fod', last_name = 'bart', bool_test = true", + "user_id = 2 AND password = 'pwd'", + ], + }); + }); + + it("returns prepared statement without WHERE given table and updates", () => { + const table = "sessions"; + const updates = { + start_time: 3408539567, + end_time: 948534534, + amended: true, + str_test: "blah blah blah", + }; + + const update = updateBuilder(table, updates); + + expect(update).to.be.deep.equal({ + query: "UPDATE ? SET ?", + params: [ + "sessions", + "start_time = 3408539567, end_time = 948534534, amended = true, str_test = 'blah blah blah'", + ], + }); + }); +}); \ No newline at end of file diff --git a/src/utils/updateBuilder.ts b/src/utils/updateBuilder.ts new file mode 100644 index 0000000..b8feff1 --- /dev/null +++ b/src/utils/updateBuilder.ts @@ -0,0 +1,31 @@ +function updateBuilder(table: string, updates: { [key: string]: any }, conditions?: { [key: string]: any }) { + let updateList: string[] = []; + Object.entries(updates).map((keyValue) => { + updateList.push(`${keyValue[0]} = ` + (typeof keyValue[1] === "string" ? `'${keyValue[1]}'` : keyValue[1])); + }); + + let conditionList: string[] = []; + if (conditions) { + Object.entries(conditions).map((keyValue) => { + conditionList.push(`${keyValue[0]} = ` + (typeof keyValue[1] === "string" ? `'${keyValue[1]}'` : keyValue[1])); + }); + } + + let query = "UPDATE ? SET ?" + let params = [ + table, + updateList.join(", "), + ]; + + if (conditionList.length > 0) { + query += " WHERE ?" + params.push(conditionList.join(" AND ")); + } + + return { + query, + params, + }; +} + +export default updateBuilder; \ No newline at end of file