From 02f7424b56b08b415d3a53d4744729f90c07d9a0 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Sun, 14 May 2023 19:17:30 +0200 Subject: [PATCH] Upgrade to proper user management --- src/models/calendar/Calendar.router.ts | 2 + .../calendar/events/credentials.service.ts | 33 +++- src/models/calendar/events/event.interface.ts | 3 +- src/models/calendar/events/events.router.ts | 51 +++-- src/models/calendar/events/events.service.ts | 13 +- .../calendar/users/session.interface.ts | 9 + src/models/calendar/users/user.interface.ts | 7 + src/models/calendar/users/users.router.ts | 125 ++++++++++++ src/models/calendar/users/users.service.ts | 182 ++++++++++++++++++ 9 files changed, 393 insertions(+), 32 deletions(-) create mode 100644 src/models/calendar/users/session.interface.ts create mode 100644 src/models/calendar/users/user.interface.ts create mode 100644 src/models/calendar/users/users.router.ts create mode 100644 src/models/calendar/users/users.service.ts diff --git a/src/models/calendar/Calendar.router.ts b/src/models/calendar/Calendar.router.ts index 770301e..c7c0e12 100644 --- a/src/models/calendar/Calendar.router.ts +++ b/src/models/calendar/Calendar.router.ts @@ -5,6 +5,7 @@ import express, {Request, Response} from 'express'; import {Guid} from 'guid-typescript'; import logger from '../../middleware/logger'; import {eventsRouter} from './events/events.router'; +import {usersRouter} from './users/users.router'; /** * Router Definition @@ -12,6 +13,7 @@ import {eventsRouter} from './events/events.router'; export const calendarRouter = express.Router(); calendarRouter.use('/events', eventsRouter); +calendarRouter.use('/users', usersRouter); calendarRouter.get('/', async (req: Request, res: Response) => { diff --git a/src/models/calendar/events/credentials.service.ts b/src/models/calendar/events/credentials.service.ts index 153bd7b..3756c96 100644 --- a/src/models/calendar/events/credentials.service.ts +++ b/src/models/calendar/events/credentials.service.ts @@ -1,4 +1,5 @@ import * as dotenv from 'dotenv'; +import * as UserService from '../users/users.service'; dotenv.config(); @@ -7,34 +8,48 @@ dotenv.config(); * Checks if the password gives admin privileges (view / create / edit / delete) * @param password */ -export const checkAdminPrivileges = (password: string) => { - return password == process.env.ADMIN_CREDENTIAL; +export const checkAdminPrivileges = async (sessionId: string, sessionKey: string, ip: string) => { + if(sessionId) { + let user = await UserService.checkSession(sessionId, sessionKey, ip); + return user.is_active; + } + return false; } /** * Checks if the password gives member view privileges * @param password */ -export const checkMemberPrivileges = (password: string) => { - return password == process.env.MEMBER_CREDENTIAL || password == process.env.ADMIN_CREDENTIAL; +export const checkMemberPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => { + if(sessionId) { + let user = await UserService.checkSession(sessionId, sessionKey, ip); + return user.is_active; + } + + return password == process.env.MEMBER_CREDENTIAL; } /** * Checks if the password gives management view privileges * @param password */ -export const checkManagementPrivileges = (password: string) => { - return password == process.env.MANAGEMENT_CREDENTIAL || password == process.env.ADMIN_CREDENTIAL; +export const checkManagementPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => { + if(sessionId) { + let user = await UserService.checkSession(sessionId, sessionKey, ip); + return user.is_active; + } + + return password == process.env.MANAGEMENT_CREDENTIAL; } -export const hasAccess = (calendarName: string, password: string) => { +export const hasAccess = async (calendarName: string, sessionId: string, sessionKey: string, password: string, ip: string) => { switch (calendarName) { case 'public': return true; case 'members': - return checkMemberPrivileges(password); + return await checkMemberPrivileges(sessionId, sessionKey, password, ip); case 'management': - return checkManagementPrivileges(password); + return await checkManagementPrivileges(sessionId, sessionKey, password, ip); default: return false; } diff --git a/src/models/calendar/events/event.interface.ts b/src/models/calendar/events/event.interface.ts index 60e25e3..7ee9058 100644 --- a/src/models/calendar/events/event.interface.ts +++ b/src/models/calendar/events/event.interface.ts @@ -8,7 +8,8 @@ export interface Event { endDateTime: Date; createdDate: Date; location: string; - createdBy: string; + createdBy?: string; + createdById: number, url: string; wholeDay: boolean; } diff --git a/src/models/calendar/events/events.router.ts b/src/models/calendar/events/events.router.ts index f87bb79..1107c2b 100644 --- a/src/models/calendar/events/events.router.ts +++ b/src/models/calendar/events/events.router.ts @@ -7,6 +7,7 @@ import {Event} from './event.interface'; import * as EventService from './events.service'; import * as iCalService from './icalgenerator.service'; import * as CredentialService from './credentials.service'; +import * as UserService from '../users/users.service'; import {Guid} from 'guid-typescript'; import logger from '../../../middleware/logger'; @@ -35,7 +36,10 @@ eventsRouter.get('/:calendar/json', async (req: Request, res: Response) => { try { // Get request params let calendarName: string = req.params.calendar as string ?? ''; + let sessionId: string = req.query.sessionId as string ?? ''; + let sessionKey: string = req.query.sessionKey as string ?? ''; let password: string = req.query.password as string ?? ''; + let ip: string = req.socket.remoteAddress ?? ''; if (calendarName.length < 1) { res.status(400).send({'message': 'Please state the name of the calendar you want events from.'}); @@ -49,7 +53,7 @@ eventsRouter.get('/:calendar/json', async (req: Request, res: Response) => { let calendarId: number = calendarNames.get(calendarName)!.id; - if (!CredentialService.hasAccess(calendarName, password)) { + if (! await CredentialService.hasAccess(calendarName, sessionId, sessionKey, password, ip)) { res.status(403).send({'message': 'You do not have access to the specified calendar.'}); return; } @@ -69,7 +73,10 @@ eventsRouter.get('/:calendar/ical', async (req: Request, res: Response) => { try { // Get request params let calendarName: string = req.params.calendar as string ?? ''; + let sessionId: string = req.query.sessionId as string ?? ''; + let sessionKey: string = req.query.sessionKey as string ?? ''; let password: string = req.query.password as string ?? ''; + let ip: string = req.socket.remoteAddress ?? ''; if (calendarName.length < 1) { res.status(400).send({'message': 'Please state the name of the calendar you want events from.'}); @@ -83,7 +90,7 @@ eventsRouter.get('/:calendar/ical', async (req: Request, res: Response) => { let calendarId: number = calendarNames.get(calendarName)!.id; - if (!CredentialService.hasAccess(calendarName, password)) { + if (! await CredentialService.hasAccess(calendarName, sessionId, sessionKey, password, ip)) { res.status(403).send({'message': 'You do not have access to the specified calendar.'}); return; } @@ -112,10 +119,14 @@ eventsRouter.get('/:calendar/ical', async (req: Request, res: Response) => { eventsRouter.post('/', async (req: Request, res: Response) => { try { // Get params - let password = req.body.password; + let sessionId: string = req.query.sessionId as string ?? ''; + let sessionKey: string = req.query.sessionKey as string ?? ''; + let ip: string = req.socket.remoteAddress ?? ''; - if (!CredentialService.checkAdminPrivileges(password)) { - res.status(403).send({'message': 'Insufficient privileges.'}); + let user = await UserService.checkSession(sessionId, sessionKey, ip); + + if (!user.is_active) { + res.status(403).send({'message': 'You do not have access to the specified calendar.'}); return; } @@ -123,8 +134,7 @@ eventsRouter.post('/', async (req: Request, res: Response) => { req.body.calendarId === undefined || isNullOrBlank(req.body.name) || req.body.startDateTime === undefined || - req.body.endDateTime === undefined || - isNullOrBlank(req.body.createdBy) + req.body.endDateTime === undefined ) { res.status(400).send({'message': 'Required parameters missing'}); return; @@ -140,7 +150,7 @@ eventsRouter.post('/', async (req: Request, res: Response) => { endDateTime: new Date(req.body.endDateTime), createdDate: new Date(), location: req.body.location ?? '', - createdBy: req.body.createdBy ?? '', + createdById: user.user_id ?? -1, url: req.body.url ?? '', wholeDay: req.body.wholeDay ?? false }; @@ -165,10 +175,14 @@ eventsRouter.post('/', async (req: Request, res: Response) => { eventsRouter.put('/:eventId', async (req: Request, res: Response) => { try { // Get params - let password = req.body.password; + let sessionId: string = req.query.sessionId as string ?? ''; + let sessionKey: string = req.query.sessionKey as string ?? ''; + let ip: string = req.socket.remoteAddress ?? ''; - if (!CredentialService.checkAdminPrivileges(password)) { - res.status(403).send({'message': 'Insufficient privileges.'}); + let user = await UserService.checkSession(sessionId, sessionKey, ip); + + if (!user.is_active) { + res.status(403).send({'message': 'You do not have access to the specified calendar.'}); return; } @@ -177,8 +191,7 @@ eventsRouter.put('/:eventId', async (req: Request, res: Response) => { req.body.calendarId === undefined || isNullOrBlank(req.body.name) || req.body.startDateTime === undefined || - req.body.endDateTime === undefined || - isNullOrBlank(req.body.createdBy) + req.body.endDateTime === undefined ) { res.status(400).send({'message': 'Required parameters missing'}); return; @@ -195,6 +208,7 @@ eventsRouter.put('/:eventId', async (req: Request, res: Response) => { createdDate: new Date(), location: req.body.location ?? '', createdBy: req.body.createdBy ?? '', + createdById: user.user_id ?? -1, url: req.body.url ?? '', wholeDay: req.body.wholeDay ?? false }; @@ -222,10 +236,14 @@ eventsRouter.put('/:eventId', async (req: Request, res: Response) => { eventsRouter.delete('/:eventId', async (req: Request, res: Response) => { try { // Get params - let password = req.body.password; + let sessionId: string = req.query.sessionId as string ?? ''; + let sessionKey: string = req.query.sessionKey as string ?? ''; + let ip: string = req.socket.remoteAddress ?? ''; - if (!CredentialService.checkAdminPrivileges(password)) { - res.status(403).send({'message': 'Insufficient privileges.'}); + let user = await UserService.checkSession(sessionId, sessionKey, ip); + + if (!user.is_active) { + res.status(403).send({'message': 'You do not have access to the specified calendar.'}); return; } @@ -247,6 +265,7 @@ eventsRouter.delete('/:eventId', async (req: Request, res: Response) => { createdDate: new Date(), location: '', createdBy: '', + createdById: -1, url: '', wholeDay: false }; diff --git a/src/models/calendar/events/events.service.ts b/src/models/calendar/events/events.service.ts index 08a161e..c9ce491 100644 --- a/src/models/calendar/events/events.service.ts +++ b/src/models/calendar/events/events.service.ts @@ -14,7 +14,7 @@ export const getAllEvents = async (calendarId: number): Promise => { let conn = await NachklangCalendarDB.getConnection(); let eventRows: Event[] = []; try { - const eventsQuery = 'SELECT * FROM events WHERE calendar_id = ?'; + const eventsQuery = 'SELECT e.*, u.full_name FROM events e LEFT OUTER JOIN users u ON e.created_by_id = u.user_id WHERE calendar_id = ?'; const eventsRes = await conn.query(eventsQuery, calendarId); for (let row of eventsRes) { @@ -28,7 +28,8 @@ export const getAllEvents = async (calendarId: number): Promise => { endDateTime: row.end_datetime, createdDate: row.created_date, location: row.location, - createdBy: row.created_by, + createdBy: row.full_name, + createdById: row.created_by_id, url: row.url, wholeDay: row.whole_day }); @@ -51,8 +52,8 @@ export const createEvent = async (event: Event): Promise => { let conn = await NachklangCalendarDB.getConnection(); try { let eventUUID = Guid.create().toString(); - const eventsQuery = 'INSERT INTO events (calendar_id, uuid, name, description, start_datetime, end_datetime, location, created_by, url, whole_day) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING event_id'; - const eventsRes = await conn.execute(eventsQuery, [event.calendarId, eventUUID, event.name, event.description, event.startDateTime, event.endDateTime, event.location, event.createdBy, event.url, event.wholeDay]); + const eventsQuery = 'INSERT INTO events (calendar_id, uuid, name, description, start_datetime, end_datetime, location, created_by_id, url, whole_day) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING event_id'; + const eventsRes = await conn.execute(eventsQuery, [event.calendarId, eventUUID, event.name, event.description, event.startDateTime, event.endDateTime, event.location, event.createdById, event.url, event.wholeDay]); return eventsRes[0].event_id; } catch (err) { @@ -72,8 +73,8 @@ export const createEvent = async (event: Event): Promise => { export const updateEvent = async (event: Event): Promise => { let conn = await NachklangCalendarDB.getConnection(); try { - const eventsQuery = 'UPDATE events SET name = ?, description = ?, start_datetime = ?, end_datetime = ?, location = ?, created_by = ?, url = ?, whole_day = ? WHERE event_id = ?'; - const eventsRes = await conn.execute(eventsQuery, [event.name, event.description, event.startDateTime, event.endDateTime, event.location, event.createdBy, event.url, event.wholeDay, event.eventId]); + const eventsQuery = 'UPDATE events SET name = ?, description = ?, start_datetime = ?, end_datetime = ?, location = ?, created_by_id = ?, url = ?, whole_day = ? WHERE event_id = ?'; + const eventsRes = await conn.execute(eventsQuery, [event.name, event.description, event.startDateTime, event.endDateTime, event.location, event.createdById, event.url, event.wholeDay, event.eventId]); return eventsRes.affectedRows; } catch (err) { diff --git a/src/models/calendar/users/session.interface.ts b/src/models/calendar/users/session.interface.ts new file mode 100644 index 0000000..9fdb6f1 --- /dev/null +++ b/src/models/calendar/users/session.interface.ts @@ -0,0 +1,9 @@ +export interface Session { + session_id: number; + user_id: number; + session_key: string; + session_key_hash: string; + created_date?: Date; + valid_until?: Date; + last_ip: string; +} diff --git a/src/models/calendar/users/user.interface.ts b/src/models/calendar/users/user.interface.ts new file mode 100644 index 0000000..5c92751 --- /dev/null +++ b/src/models/calendar/users/user.interface.ts @@ -0,0 +1,7 @@ +export interface User { + user_id: number; + full_name: string; + password_hash: string; + email: string; + is_active: boolean; +} diff --git a/src/models/calendar/users/users.router.ts b/src/models/calendar/users/users.router.ts new file mode 100644 index 0000000..5c649b7 --- /dev/null +++ b/src/models/calendar/users/users.router.ts @@ -0,0 +1,125 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as UserService from './users.service'; +import {Session} from './session.interface'; +import {User} from './user.interface'; +import {Guid} from 'guid-typescript'; +import logger from '../../../middleware/logger'; + +/** + * Router Definition + */ + +export const usersRouter = express.Router(); + + +/** + * Controller Definitions + */ + +// POST users/register +usersRouter.post('/register', async (req: Request, res: Response) => { + try { + const password: string = req.body.password; + const email: string = req.body.email; + const fullName: string = req.body.fullName; + const ip: string = req.socket.remoteAddress ?? ''; + + if (!password || !email) { + // Missing + res.status(400).send(JSON.stringify({message: 'Missing parameters'})); + return; + } + + // Create the user and a session + const session: Session = await UserService.createUser(email, password, fullName, ip); + + // Send the session details back to the user + res.status(201).send({ + session_id: session.session_id, + session_key: session.session_key + }); + } catch (e: any) { + let errorGuid = Guid.create().toString(); + logger.error('Error handling a request: ' + e.message, {reference: errorGuid}); + res.status(500).send({ + 'status': 'PROCESSING_ERROR', + 'message': 'Internal Server Error. Try again later.', + 'reference': errorGuid + }); + } +}); + +// POST users/login +usersRouter.post('/login', async (req: Request, res: Response) => { + try { + const password: string = req.body.password; + const email: string = req.body.email; + const ip: string = req.socket.remoteAddress ?? ''; + + if (!password || !email) { + // Missing + res.status(400).send(JSON.stringify({message: 'Missing parameters'})); + return; + } + + // Create a session + const session: Session = await UserService.login(email, password, ip); + + if (!session.session_id) { + // Error logging in, probably wrong username / password + res.status(401).send(JSON.stringify({messages: ['Wrong username and / or password']})); + return; + } + + // Send the session details back to the user + res.status(200).send({ + session_id: session.session_id, + session_key: session.session_key + }); + } catch (e: any) { + let errorGuid = Guid.create().toString(); + logger.error('Error handling a request: ' + e.message, {reference: errorGuid}); + res.status(500).send({ + 'status': 'PROCESSING_ERROR', + 'message': 'Internal Server Error. Try again later.', + 'reference': errorGuid + }); + } +}); + +// POST users/checkSessionValid +usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => { + try { + const ip: string = req.socket.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + + if (!session_id || !session_key) { + // Error logging in, probably wrong username / password + res.status(401).send(JSON.stringify({messages: ['No session detected']})); + return; + } + + const user: User = await UserService.checkSession(session_id, session_key, ip); + + if (!user.user_id) { + // Error logging in, probably wrong username / password + res.status(401).send(JSON.stringify({messages: ['Invalid session']})); + return; + } + + res.status(200).send(user); + } catch (e: any) { + let errorGuid = Guid.create().toString(); + logger.error('Error handling a request: ' + e.message, {reference: errorGuid}); + res.status(500).send({ + 'status': 'PROCESSING_ERROR', + 'message': 'Internal Server Error. Try again later.', + 'reference': errorGuid + }); + } +}); diff --git a/src/models/calendar/users/users.service.ts b/src/models/calendar/users/users.service.ts new file mode 100644 index 0000000..f3d7098 --- /dev/null +++ b/src/models/calendar/users/users.service.ts @@ -0,0 +1,182 @@ +import * as dotenv from 'dotenv'; +import * as bcrypt from 'bcrypt'; +import {Guid} from 'guid-typescript'; +import {User} from './user.interface'; +import {Session} from './session.interface'; +import {NachklangCalendarDB} from '../Calendar.db'; + +dotenv.config(); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Creates a user record in the database, also creates a session. Returns the session if successful. + */ +export const createUser = async (email: string, password: string, fullName: string, ip: string): Promise => { + let conn = await NachklangCalendarDB.getConnection(); + try { + // Hash password and generate + hash session key + const pwHash = bcrypt.hashSync(password, 10); + const sessionKey = Guid.create().toString(); + const sessionKeyHash = bcrypt.hashSync(sessionKey, 10); + + // Create user entry in SQL + const userQuery = 'INSERT INTO users (email, password_hash, full_name) VALUES (?, ?, ?) RETURNING user_id'; + const userIdRes = await conn.query(userQuery, [email, pwHash, fullName]); + + // Get user id of the created user + let userId: number = -1; + for (const row of userIdRes) { + userId = row.user_id; + } + + // Create session + const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, created_date, valid_until, last_ip) VALUES (?,?,NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),?) RETURNING session_id'; + const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]); + await conn.commit(); + + // Get session id of the created session + let sessionId: number = -1; + for (const row of sessionIdRes) { + sessionId = row.session_id; + } + + return { + session_id: sessionId, + user_id: userId, + session_key: sessionKey, + session_key_hash: 'HIDDEN', + last_ip: ip + }; + } catch (err) { + throw err; + } finally { + // Return connection + await conn.end(); + } +}; + +/** + * Checks if the given credentials are valid and creates a new session if they are. + * Returns the session information in case of a successful login + */ +export const login = async (email: string, password: string, ip: string): Promise => { + let conn = await NachklangCalendarDB.getConnection(); + try { + // Get saved password hash + const query = 'SELECT user_id, password_hash FROM users WHERE email = ?'; + const userRows = await conn.query(query, email); + let savedHash = ''; + let userId = -1; + for (const row of userRows) { + savedHash = row.password_hash; + userId = row.user_id; + } + + // Check for correct password + if (!bcrypt.compareSync(password, savedHash)) { + // Wrong password, return invalid + return {} as Session; + } + + // Generate + hash session key + const sessionKey = Guid.create().toString(); + const sessionKeyHash = bcrypt.hashSync(sessionKey, 10); + + // Create session + const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, created_date, valid_until, last_ip) VALUES (?,?,NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),?) RETURNING session_id'; + const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]); + await conn.commit(); + + // Get session id of the created session + let sessionId: number = -1; + for (const row of sessionIdRes) { + sessionId = row.session_id; + } + + return { + session_id: sessionId, + user_id: userId, + session_key: sessionKey, + session_key_hash: 'HIDDEN', + last_ip: ip + }; + } catch (err) { + throw err; + } finally { + // Return connection + await conn.end(); + } +}; + +/** + * Checks if the given session information are valid and returns the user information if they are + */ +export const checkSession = async (sessionId: string, sessionKey: string, ip: string): Promise => { + let conn = await NachklangCalendarDB.getConnection(); + try { + // Get saved session key hash + const query = 'SELECT user_id, session_key_hash, valid_until FROM sessions WHERE session_id = ?'; + const sessionRows = await conn.query(query, sessionId); + let savedHash = ''; + let userId = -1; + let validUntil = new Date(); + for (const row of sessionRows) { + savedHash = row.session_key_hash; + userId = row.user_id; + validUntil = row.valid_until; + } + + // Check for correct key + if (!bcrypt.compareSync(sessionKey, savedHash)) { + // Wrong key, return invalid + return {} as User; + } + + // Check if the session is still valid + if (validUntil <= new Date()) { + // Session expired, return invalid + return {} as User; + } + + // Update session entry in SQL + const updateSessionsQuery = 'UPDATE sessions SET last_IP = ? WHERE session_id = ?'; + const userIdRes = await conn.query(updateSessionsQuery, [ip, sessionId]); + await conn.commit(); + + // Get the other required user information + const userQuery = 'SELECT user_id, email, full_name, is_active FROM users WHERE user_id = ?'; + const userRows = await conn.query(userQuery, userId); + let username = ''; + let email = ''; + let fullName = ''; + let is_active = false; + for (const row of userRows) { + username = row.username; + email = row.email; + fullName = row.full_name; + is_active = row.is_active; + } + + // Everything is fine, return user information + return { + user_id: userId, + email: email, + password_hash: 'HIDDEN', + full_name: fullName, + is_active: is_active + }; + } catch (err) { + throw err; + } finally { + // Return connection + await conn.end(); + } +};