mirror of
https://github.com/Mueller-Patrick/Betterzon.git
synced 2024-11-22 14:23:57 +00:00
BETTERZON-75: User registration API endpoint (#34)
* BETTERZON-75: Adding backend functions to enable user registration * BETTERZON-75: Adding regex to check email and username
This commit is contained in:
parent
f7c045b5a3
commit
a3ac897818
816
Backend/package-lock.json
generated
816
Backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -11,14 +11,17 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcrypt": "^5.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"guid-typescript": "^1.0.9",
|
||||||
"helmet": "^4.2.0",
|
"helmet": "^4.2.0",
|
||||||
"mariadb": "^2.5.1",
|
"mariadb": "^2.5.1",
|
||||||
"typeorm": "^0.2.29"
|
"typeorm": "^0.2.29"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^3.0.1",
|
||||||
"@types/cors": "^2.8.8",
|
"@types/cors": "^2.8.8",
|
||||||
"@types/dotenv": "^8.2.0",
|
"@types/dotenv": "^8.2.0",
|
||||||
"@types/express": "^4.17.9",
|
"@types/express": "^4.17.9",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {pricesRouter} from './models/prices/prices.router';
|
||||||
import {vendorsRouter} from './models/vendors/vendors.router';
|
import {vendorsRouter} from './models/vendors/vendors.router';
|
||||||
import {errorHandler} from './middleware/error.middleware';
|
import {errorHandler} from './middleware/error.middleware';
|
||||||
import {notFoundHandler} from './middleware/notFound.middleware';
|
import {notFoundHandler} from './middleware/notFound.middleware';
|
||||||
|
import {usersRouter} from './models/users/users.router';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ app.use('/products', productsRouter);
|
||||||
app.use('/categories', categoriesRouter);
|
app.use('/categories', categoriesRouter);
|
||||||
app.use('/manufacturers', manufacturersRouter);
|
app.use('/manufacturers', manufacturersRouter);
|
||||||
app.use('/prices', pricesRouter);
|
app.use('/prices', pricesRouter);
|
||||||
|
app.use('/users', usersRouter);
|
||||||
app.use('/vendors', vendorsRouter);
|
app.use('/vendors', vendorsRouter);
|
||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
10
Backend/src/models/users/session.interface.ts
Normal file
10
Backend/src/models/users/session.interface.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export interface Session {
|
||||||
|
session_id: number;
|
||||||
|
session_key: string;
|
||||||
|
session_key_hash: string;
|
||||||
|
createdDate?: Date;
|
||||||
|
lastLogin?: Date;
|
||||||
|
validUntil?: Date;
|
||||||
|
validDays?: number;
|
||||||
|
last_IP: string;
|
||||||
|
}
|
9
Backend/src/models/users/user.interface.ts
Normal file
9
Backend/src/models/users/user.interface.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export interface User {
|
||||||
|
user_id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password_hash: string;
|
||||||
|
hashing_salt: string;
|
||||||
|
registration_date: Date;
|
||||||
|
last_login_date: Date;
|
||||||
|
}
|
5
Backend/src/models/users/users.interface.ts
Normal file
5
Backend/src/models/users/users.interface.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import {User} from './user.interface';
|
||||||
|
|
||||||
|
export interface Users {
|
||||||
|
[key: number]: User;
|
||||||
|
}
|
54
Backend/src/models/users/users.router.ts
Normal file
54
Backend/src/models/users/users.router.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* Required External Modules and Interfaces
|
||||||
|
*/
|
||||||
|
|
||||||
|
import express, {Request, Response} from 'express';
|
||||||
|
import * as UserService from './users.service';
|
||||||
|
import {User} from './user.interface';
|
||||||
|
import {Users} from './users.interface';
|
||||||
|
import {Session} from './session.interface';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router Definition
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const usersRouter = express.Router();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller Definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// POST users/register
|
||||||
|
usersRouter.post('/register', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const username: string = req.body.username;
|
||||||
|
const password: string = req.body.password;
|
||||||
|
const email: string = req.body.email;
|
||||||
|
const ip: string = req.connection.remoteAddress?? '';
|
||||||
|
|
||||||
|
if (!username || !password || !email) {
|
||||||
|
// Missing
|
||||||
|
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if username and / or email are already used
|
||||||
|
const status = await UserService.checkUsernameAndEmail(username, email);
|
||||||
|
|
||||||
|
if (status.hasProblems) {
|
||||||
|
// Username and/or email are duplicates, return error
|
||||||
|
res.status(400).send(JSON.stringify({messages: status.messages, codes: status.codes}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the user and a session
|
||||||
|
const session: Session = await UserService.createUser(username, password, email, ip);
|
||||||
|
|
||||||
|
// Send the session details back to the user
|
||||||
|
res.status(201).send(session);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(404).send(e.message);
|
||||||
|
}
|
||||||
|
});
|
157
Backend/src/models/users/users.service.ts
Normal file
157
Backend/src/models/users/users.service.ts
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import {Guid} from 'guid-typescript';
|
||||||
|
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const mariadb = require('mariadb');
|
||||||
|
const pool = mariadb.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_DATABASE,
|
||||||
|
connectionLimit: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Model Interfaces
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {User} from './user.interface';
|
||||||
|
import {Users} from './users.interface';
|
||||||
|
import {Session} from './session.interface';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a user record in the database, also creates a session. Returns the session if successful.
|
||||||
|
*/
|
||||||
|
export const createUser = async (username: string, password: string, email: string, ip: string): Promise<Session> => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
// Hash password and generate + hash session key
|
||||||
|
const pwHash = bcrypt.hashSync('123', 10);
|
||||||
|
const sessionKey = Guid.create().toString();
|
||||||
|
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
|
||||||
|
|
||||||
|
// Create user entry in SQL
|
||||||
|
conn = await pool.getConnection();
|
||||||
|
const userQuery = 'INSERT INTO users (username, email, bcrypt_password_hash) VALUES (?, ?, ?) RETURNING user_id';
|
||||||
|
const userIdRes = await conn.query(userQuery, [username, email, pwHash]);
|
||||||
|
await conn.commit();
|
||||||
|
|
||||||
|
// Get user id of the created user
|
||||||
|
let userId: number = -1;
|
||||||
|
for (const row in userIdRes) {
|
||||||
|
if (row !== 'meta' && userIdRes[row].user_id != null) {
|
||||||
|
userId = userIdRes[row].user_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, createdDate, lastLogin, validUntil, validDays, last_IP) VALUES (?,?,NOW(),NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),30,?) RETURNING session_id';
|
||||||
|
const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]);
|
||||||
|
await conn.commit();
|
||||||
|
|
||||||
|
// Get session id of the created user
|
||||||
|
let sessionId: number = -1;
|
||||||
|
for (const row in sessionIdRes) {
|
||||||
|
if (row !== 'meta' && sessionIdRes[row].session_id != null) {
|
||||||
|
sessionId = sessionIdRes[row].session_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
session_id: sessionId,
|
||||||
|
session_key: sessionKey,
|
||||||
|
session_key_hash: '',
|
||||||
|
last_IP: ip
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {} as Session;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in the checkUsernameAndEmail method as return value
|
||||||
|
*/
|
||||||
|
export interface Status {
|
||||||
|
hasProblems: boolean;
|
||||||
|
messages: string[];
|
||||||
|
codes: number[]; // 0 = all good, 1 = wrong username, 2 = wrong email, 3 = server error
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given username and email are not used yet by another user
|
||||||
|
* @param username The username to check
|
||||||
|
* @param email The email to check
|
||||||
|
*/
|
||||||
|
export const checkUsernameAndEmail = async (username: string, email: string): Promise<Status> => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
// Create user entry in SQL
|
||||||
|
conn = await pool.getConnection();
|
||||||
|
const usernameQuery = 'SELECT username FROM users WHERE username = ?';
|
||||||
|
const emailQuery = 'SELECT email FROM users WHERE email = ?';
|
||||||
|
const usernameRes = await conn.query(usernameQuery, username);
|
||||||
|
const emailRes = await conn.query(emailQuery, email);
|
||||||
|
|
||||||
|
let res: Status = {
|
||||||
|
hasProblems: false,
|
||||||
|
messages: [],
|
||||||
|
codes: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const usernameRegex = RegExp('^[a-zA-Z0-9\\-\\_]{4,20}$'); // Can contain a-z, A-Z, 0-9, -, _ and has to be 4-20 chars long
|
||||||
|
if (!usernameRegex.test(username)) {
|
||||||
|
// Username doesn't match requirements
|
||||||
|
res.hasProblems = true;
|
||||||
|
res.messages.push('Invalid username');
|
||||||
|
res.codes.push(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRegex = RegExp('^[a-zA-Z0-9\\-\\_.]{1,30}\\@[a-zA-Z0-9\\-.]{1,20}\\.[a-z]{1,20}$'); // Normal email regex, user@betterzon.xyz
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
// Username doesn't match requirements
|
||||||
|
res.hasProblems = true;
|
||||||
|
res.messages.push('Invalid email');
|
||||||
|
res.codes.push(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usernameRes.length > 0) {
|
||||||
|
// Username is a duplicate
|
||||||
|
res.hasProblems = true;
|
||||||
|
res.messages.push('Duplicate username');
|
||||||
|
res.codes.push(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailRes.length > 0) {
|
||||||
|
// Email is a duplicate
|
||||||
|
res.hasProblems = true;
|
||||||
|
res.messages.push('Duplicate email');
|
||||||
|
res.codes.push(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {hasProblems: true, messages: ['Internal server error'], codes: [3]};
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user