Upgrade to proper user management

This commit is contained in:
Patrick Müller 2023-05-14 19:17:30 +02:00
parent 93c70b0e1d
commit 02f7424b56
Signed by: Paddy
GPG Key ID: 37ABC11275CAABCE
9 changed files with 393 additions and 32 deletions

View File

@ -5,6 +5,7 @@ import express, {Request, Response} from 'express';
import {Guid} from 'guid-typescript'; import {Guid} from 'guid-typescript';
import logger from '../../middleware/logger'; import logger from '../../middleware/logger';
import {eventsRouter} from './events/events.router'; import {eventsRouter} from './events/events.router';
import {usersRouter} from './users/users.router';
/** /**
* Router Definition * Router Definition
@ -12,6 +13,7 @@ import {eventsRouter} from './events/events.router';
export const calendarRouter = express.Router(); export const calendarRouter = express.Router();
calendarRouter.use('/events', eventsRouter); calendarRouter.use('/events', eventsRouter);
calendarRouter.use('/users', usersRouter);
calendarRouter.get('/', async (req: Request, res: Response) => { calendarRouter.get('/', async (req: Request, res: Response) => {

View File

@ -1,4 +1,5 @@
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import * as UserService from '../users/users.service';
dotenv.config(); dotenv.config();
@ -7,34 +8,48 @@ dotenv.config();
* Checks if the password gives admin privileges (view / create / edit / delete) * Checks if the password gives admin privileges (view / create / edit / delete)
* @param password * @param password
*/ */
export const checkAdminPrivileges = (password: string) => { export const checkAdminPrivileges = async (sessionId: string, sessionKey: string, ip: string) => {
return password == process.env.ADMIN_CREDENTIAL; if(sessionId) {
let user = await UserService.checkSession(sessionId, sessionKey, ip);
return user.is_active;
}
return false;
} }
/** /**
* Checks if the password gives member view privileges * Checks if the password gives member view privileges
* @param password * @param password
*/ */
export const checkMemberPrivileges = (password: string) => { export const checkMemberPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
return password == process.env.MEMBER_CREDENTIAL || password == process.env.ADMIN_CREDENTIAL; 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 * Checks if the password gives management view privileges
* @param password * @param password
*/ */
export const checkManagementPrivileges = (password: string) => { export const checkManagementPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
return password == process.env.MANAGEMENT_CREDENTIAL || password == process.env.ADMIN_CREDENTIAL; if(sessionId) {
let user = await UserService.checkSession(sessionId, sessionKey, ip);
return user.is_active;
} }
export const hasAccess = (calendarName: string, password: string) => { return password == process.env.MANAGEMENT_CREDENTIAL;
}
export const hasAccess = async (calendarName: string, sessionId: string, sessionKey: string, password: string, ip: string) => {
switch (calendarName) { switch (calendarName) {
case 'public': case 'public':
return true; return true;
case 'members': case 'members':
return checkMemberPrivileges(password); return await checkMemberPrivileges(sessionId, sessionKey, password, ip);
case 'management': case 'management':
return checkManagementPrivileges(password); return await checkManagementPrivileges(sessionId, sessionKey, password, ip);
default: default:
return false; return false;
} }

View File

@ -8,7 +8,8 @@ export interface Event {
endDateTime: Date; endDateTime: Date;
createdDate: Date; createdDate: Date;
location: string; location: string;
createdBy: string; createdBy?: string;
createdById: number,
url: string; url: string;
wholeDay: boolean; wholeDay: boolean;
} }

View File

@ -7,6 +7,7 @@ import {Event} from './event.interface';
import * as EventService from './events.service'; import * as EventService from './events.service';
import * as iCalService from './icalgenerator.service'; import * as iCalService from './icalgenerator.service';
import * as CredentialService from './credentials.service'; import * as CredentialService from './credentials.service';
import * as UserService from '../users/users.service';
import {Guid} from 'guid-typescript'; import {Guid} from 'guid-typescript';
import logger from '../../../middleware/logger'; import logger from '../../../middleware/logger';
@ -35,7 +36,10 @@ eventsRouter.get('/:calendar/json', async (req: Request, res: Response) => {
try { try {
// Get request params // Get request params
let calendarName: string = req.params.calendar as string ?? ''; 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 password: string = req.query.password as string ?? '';
let ip: string = req.socket.remoteAddress ?? '';
if (calendarName.length < 1) { if (calendarName.length < 1) {
res.status(400).send({'message': 'Please state the name of the calendar you want events from.'}); 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; 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.'}); res.status(403).send({'message': 'You do not have access to the specified calendar.'});
return; return;
} }
@ -69,7 +73,10 @@ eventsRouter.get('/:calendar/ical', async (req: Request, res: Response) => {
try { try {
// Get request params // Get request params
let calendarName: string = req.params.calendar as string ?? ''; 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 password: string = req.query.password as string ?? '';
let ip: string = req.socket.remoteAddress ?? '';
if (calendarName.length < 1) { if (calendarName.length < 1) {
res.status(400).send({'message': 'Please state the name of the calendar you want events from.'}); 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; 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.'}); res.status(403).send({'message': 'You do not have access to the specified calendar.'});
return; return;
} }
@ -112,10 +119,14 @@ eventsRouter.get('/:calendar/ical', async (req: Request, res: Response) => {
eventsRouter.post('/', async (req: Request, res: Response) => { eventsRouter.post('/', async (req: Request, res: Response) => {
try { try {
// Get params // 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)) { let user = await UserService.checkSession(sessionId, sessionKey, ip);
res.status(403).send({'message': 'Insufficient privileges.'});
if (!user.is_active) {
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
return; return;
} }
@ -123,8 +134,7 @@ eventsRouter.post('/', async (req: Request, res: Response) => {
req.body.calendarId === undefined || req.body.calendarId === undefined ||
isNullOrBlank(req.body.name) || isNullOrBlank(req.body.name) ||
req.body.startDateTime === undefined || req.body.startDateTime === undefined ||
req.body.endDateTime === undefined || req.body.endDateTime === undefined
isNullOrBlank(req.body.createdBy)
) { ) {
res.status(400).send({'message': 'Required parameters missing'}); res.status(400).send({'message': 'Required parameters missing'});
return; return;
@ -140,7 +150,7 @@ eventsRouter.post('/', async (req: Request, res: Response) => {
endDateTime: new Date(req.body.endDateTime), endDateTime: new Date(req.body.endDateTime),
createdDate: new Date(), createdDate: new Date(),
location: req.body.location ?? '', location: req.body.location ?? '',
createdBy: req.body.createdBy ?? '', createdById: user.user_id ?? -1,
url: req.body.url ?? '', url: req.body.url ?? '',
wholeDay: req.body.wholeDay ?? false wholeDay: req.body.wholeDay ?? false
}; };
@ -165,10 +175,14 @@ eventsRouter.post('/', async (req: Request, res: Response) => {
eventsRouter.put('/:eventId', async (req: Request, res: Response) => { eventsRouter.put('/:eventId', async (req: Request, res: Response) => {
try { try {
// Get params // 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)) { let user = await UserService.checkSession(sessionId, sessionKey, ip);
res.status(403).send({'message': 'Insufficient privileges.'});
if (!user.is_active) {
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
return; return;
} }
@ -177,8 +191,7 @@ eventsRouter.put('/:eventId', async (req: Request, res: Response) => {
req.body.calendarId === undefined || req.body.calendarId === undefined ||
isNullOrBlank(req.body.name) || isNullOrBlank(req.body.name) ||
req.body.startDateTime === undefined || req.body.startDateTime === undefined ||
req.body.endDateTime === undefined || req.body.endDateTime === undefined
isNullOrBlank(req.body.createdBy)
) { ) {
res.status(400).send({'message': 'Required parameters missing'}); res.status(400).send({'message': 'Required parameters missing'});
return; return;
@ -195,6 +208,7 @@ eventsRouter.put('/:eventId', async (req: Request, res: Response) => {
createdDate: new Date(), createdDate: new Date(),
location: req.body.location ?? '', location: req.body.location ?? '',
createdBy: req.body.createdBy ?? '', createdBy: req.body.createdBy ?? '',
createdById: user.user_id ?? -1,
url: req.body.url ?? '', url: req.body.url ?? '',
wholeDay: req.body.wholeDay ?? false 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) => { eventsRouter.delete('/:eventId', async (req: Request, res: Response) => {
try { try {
// Get params // 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)) { let user = await UserService.checkSession(sessionId, sessionKey, ip);
res.status(403).send({'message': 'Insufficient privileges.'});
if (!user.is_active) {
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
return; return;
} }
@ -247,6 +265,7 @@ eventsRouter.delete('/:eventId', async (req: Request, res: Response) => {
createdDate: new Date(), createdDate: new Date(),
location: '', location: '',
createdBy: '', createdBy: '',
createdById: -1,
url: '', url: '',
wholeDay: false wholeDay: false
}; };

View File

@ -14,7 +14,7 @@ export const getAllEvents = async (calendarId: number): Promise<Event[]> => {
let conn = await NachklangCalendarDB.getConnection(); let conn = await NachklangCalendarDB.getConnection();
let eventRows: Event[] = []; let eventRows: Event[] = [];
try { 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); const eventsRes = await conn.query(eventsQuery, calendarId);
for (let row of eventsRes) { for (let row of eventsRes) {
@ -28,7 +28,8 @@ export const getAllEvents = async (calendarId: number): Promise<Event[]> => {
endDateTime: row.end_datetime, endDateTime: row.end_datetime,
createdDate: row.created_date, createdDate: row.created_date,
location: row.location, location: row.location,
createdBy: row.created_by, createdBy: row.full_name,
createdById: row.created_by_id,
url: row.url, url: row.url,
wholeDay: row.whole_day wholeDay: row.whole_day
}); });
@ -51,8 +52,8 @@ export const createEvent = async (event: Event): Promise<number> => {
let conn = await NachklangCalendarDB.getConnection(); let conn = await NachklangCalendarDB.getConnection();
try { try {
let eventUUID = Guid.create().toString(); 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 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.createdBy, event.url, event.wholeDay]); 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; return eventsRes[0].event_id;
} catch (err) { } catch (err) {
@ -72,8 +73,8 @@ export const createEvent = async (event: Event): Promise<number> => {
export const updateEvent = async (event: Event): Promise<number> => { export const updateEvent = async (event: Event): Promise<number> => {
let conn = await NachklangCalendarDB.getConnection(); let conn = await NachklangCalendarDB.getConnection();
try { try {
const eventsQuery = 'UPDATE events SET name = ?, description = ?, start_datetime = ?, end_datetime = ?, location = ?, created_by = ?, url = ?, whole_day = ? WHERE event_id = ?'; 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.createdBy, event.url, event.wholeDay, event.eventId]); 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; return eventsRes.affectedRows;
} catch (err) { } catch (err) {

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
export interface User {
user_id: number;
full_name: string;
password_hash: string;
email: string;
is_active: boolean;
}

View File

@ -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
});
}
});

View File

@ -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<Session> => {
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<Session> => {
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<User> => {
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();
}
};