diff --git a/src/api/middlewares/authMiddleware.js b/src/api/middlewares/authMiddleware.js
new file mode 100644
index 0000000..4cdbb51
--- /dev/null
+++ b/src/api/middlewares/authMiddleware.js
@@ -0,0 +1,25 @@
+import { validateJwtToken } from '#util/jwtUtils.js';
+import { DbUser } from '#db/schemas/index.js';
+
+/**
+ * @param options {AuthValidationOptions}
+ * @returns {(function(*, *, *): void)|*}
+ */
+export function validateAuth(options) {
+ return async function (req, res, next) {
+ var auth = validateJwtToken(req.headers.authorization);
+ if (!auth) {
+ res.status(401).send('Unauthorized');
+ return;
+ }
+
+ req.user = await DbUser.findById(auth.id).exec();
+
+ req.auth = auth;
+ req = next();
+ };
+}
+
+class AuthValidationOptions {
+ roles;
+}
diff --git a/src/api/middlewares/index.js b/src/api/middlewares/index.js
index 1894f1a..f712465 100644
--- a/src/api/middlewares/index.js
+++ b/src/api/middlewares/index.js
@@ -1,2 +1,4 @@
export * from './corsMiddleware.js';
export * from './loggingMiddleware.js';
+export * from './errorMiddleware.js';
+export * from './authMiddleware.js';
diff --git a/src/api/routes.js b/src/api/routes.js
index 73d954e..0da8be9 100644
--- a/src/api/routes.js
+++ b/src/api/routes.js
@@ -2,6 +2,7 @@ import * as routes from './routes/index.js';
export function registerRoutes(app) {
// app.get("/status", routes.statusRoute);
+ let routeCount = 0;
Object.values(routes).forEach(route => {
console.log('Registering route:', route);
if (!route.route)
@@ -10,9 +11,26 @@ export function registerRoutes(app) {
JSON.stringify(route)
);
- if (route.onGet) app.get(route.route, route.onGet);
- if (route.onPost) app.post(route.route, route.onPost);
- if (route.onPut) app.put(route.route, route.onPut);
- if (route.onDelete) app.put(route.route, route.onDelete);
+ if (route.onGet) {
+ app.get(route.route, route.onGet);
+ routeCount++;
+ }
+ if (route.onPost) {
+ app.post(route.route, route.onPost);
+ routeCount++;
+ }
+ if (route.onPut) {
+ app.put(route.route, route.onPut);
+ routeCount++;
+ }
+ if (route.onDelete) {
+ app.put(route.route, route.onDelete);
+ routeCount++;
+ }
+ if (route.onPatch) {
+ app.patch(route.route, route.onPatch);
+ routeCount++;
+ }
});
+ console.log(`Registered ${routeCount} routes.`);
}
diff --git a/src/api/routes/auth/accountRoutes.js b/src/api/routes/auth/accountRoutes.js
new file mode 100644
index 0000000..6655ecb
--- /dev/null
+++ b/src/api/routes/auth/accountRoutes.js
@@ -0,0 +1,36 @@
+import { deleteUser, loginUser, registerUser } from '#db/index.js';
+import { AuthDto, RegisterDto } from '#dto/index.js';
+
+export const registerRoute = {
+ route: '/auth/register',
+ async onPost(req, res) {
+ const data = await RegisterDto.create(req.body);
+ const registerResult = await registerUser(data);
+ res.send(registerResult);
+ }
+};
+
+export const loginRoute = {
+ route: '/auth/login',
+ /**
+ *
+ * @param req {Request}
+ * @param res
+ * @returns {Promise<WhoAmIDto>}
+ */
+ async onPost(req, res) {
+ const data = await AuthDto.create(req.body);
+ console.log(req.headers['user-agent']);
+ const loginResult = await loginUser(data, req.headers['user-agent']);
+ res.send(loginResult);
+ }
+};
+
+export const deleteRoute = {
+ route: '/auth/delete',
+ async onDelete(req, res) {
+ const data = await AuthDto.create(req.body);
+ await deleteUser(data);
+ res.status(204).send();
+ }
+};
diff --git a/src/api/routes/auth/deviceRoutes.js b/src/api/routes/auth/deviceRoutes.js
new file mode 100644
index 0000000..6655ecb
--- /dev/null
+++ b/src/api/routes/auth/deviceRoutes.js
@@ -0,0 +1,36 @@
+import { deleteUser, loginUser, registerUser } from '#db/index.js';
+import { AuthDto, RegisterDto } from '#dto/index.js';
+
+export const registerRoute = {
+ route: '/auth/register',
+ async onPost(req, res) {
+ const data = await RegisterDto.create(req.body);
+ const registerResult = await registerUser(data);
+ res.send(registerResult);
+ }
+};
+
+export const loginRoute = {
+ route: '/auth/login',
+ /**
+ *
+ * @param req {Request}
+ * @param res
+ * @returns {Promise<WhoAmIDto>}
+ */
+ async onPost(req, res) {
+ const data = await AuthDto.create(req.body);
+ console.log(req.headers['user-agent']);
+ const loginResult = await loginUser(data, req.headers['user-agent']);
+ res.send(loginResult);
+ }
+};
+
+export const deleteRoute = {
+ route: '/auth/delete',
+ async onDelete(req, res) {
+ const data = await AuthDto.create(req.body);
+ await deleteUser(data);
+ res.status(204).send();
+ }
+};
diff --git a/src/api/routes/auth/index.js b/src/api/routes/auth/index.js
index 7113a17..29a07ad 100644
--- a/src/api/routes/auth/index.js
+++ b/src/api/routes/auth/index.js
@@ -1 +1 @@
-export * from './registerRoute.js';
+export * from './accountRoutes.js';
diff --git a/src/api/routes/auth/registerRoute.js b/src/api/routes/auth/registerRoute.js
deleted file mode 100644
index 87762d3..0000000
--- a/src/api/routes/auth/registerRoute.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { registerUser } from '#db/index.js';
-import { LoginDto, RegisterDto } from '#dto/index.js';
-
-export const registerRoute = {
- route: '/auth/register',
- async onPost(req, res) {
- const data = await RegisterDto.create(req.body);
- await registerUser(data);
- res.send(data);
- }
-};
-
-export const loginRoute = {
- route: '/auth/login',
- async onPost(req, res) {
- const data = await LoginDto.create(req.body);
- await registerUser(data);
- res.send(data);
- }
-};
diff --git a/src/db/db.js b/src/db/db.js
index b9a425c..2035731 100644
--- a/src/db/db.js
+++ b/src/db/db.js
@@ -1,11 +1,14 @@
-import { connect } from 'mongoose';
+import mongoose, { connect } from 'mongoose';
import { readSecret } from '#util/secretUtils.js';
export async function initDb() {
const connectionString = await readSecret(
- "MongoDB connection string",
+ 'MongoDB connection string',
process.env['DATABASE_SECRET_PATH']
);
+
+ if (process.env['LOG_QUERIES']) mongoose.set('debug', true);
+
try {
const res = await connect(connectionString);
if (res.connection.readyState === 1) {
diff --git a/src/db/dbAccess/user.js b/src/db/dbAccess/user.js
index 6301cb5..a461f3e 100644
--- a/src/db/dbAccess/user.js
+++ b/src/db/dbAccess/user.js
@@ -1,16 +1,53 @@
-import { hash, compare } from 'bcrypt';
-import { DbUser } from '#db/schemas/index.js';
-import { RegisterDto } from '#dto/auth/index.js';
+import { hash, compare, genSalt } from 'bcrypt';
+import { DbUser, deviceSchema } from '#db/schemas/index.js';
+import { AuthDto, RegisterDto } from '#dto/index.js';
+import { SafeNSoundError } from '#util/error.js';
+import { WhoAmIDto } from '#dto/auth/WhoAmIDto.js';
+
+async function whoAmI(token) {}
+
+async function getUserByAuth(data) {
+ if (!(data instanceof AuthDto))
+ throw new Error('Invalid data type. Expected AuthDto.');
+
+ let user;
+
+ if (data.email) {
+ user = await DbUser.findOne({ email: data.email });
+ } else if (data.username) {
+ user = await DbUser.findOne({ username: data.username });
+ }
+
+ console.log('user', user);
+ if (!user) {
+ // Sneaky: prevent user enumeration
+ throw new SafeNSoundError({
+ errCode: 'INVALID_AUTH',
+ message: 'Invalid username or password.'
+ });
+ }
+
+ const isPasswordValid = await compare(data.password, user.passwordHash);
+ if (!isPasswordValid) {
+ throw new SafeNSoundError({
+ errCode: 'INVALID_AUTH',
+ message: 'Invalid username or password.'
+ });
+ }
+
+ return user;
+}
/**
* @param data {RegisterDto}
- * @returns {Promise<(Error | HydratedDocument<InferSchemaType<module:mongoose.Schema>, ObtainSchemaGeneric<module:mongoose.Schema, "TVirtuals"> & ObtainSchemaGeneric<module:mongoose.Schema, "TInstanceMethods">, ObtainSchemaGeneric<module:mongoose.Schema, "TQueryHelpers">, ObtainSchemaGeneric<module:mongoose.Schema, "TVirtuals">>)[]>}
+ * @returns {Promise<DbUser>}
*/
export async function registerUser(data) {
if (!(data instanceof RegisterDto))
throw new Error('Invalid data type. Expected RegisterDto.');
- const passwordHash = await hash(data.password, 10);
+ const salt = await genSalt(12);
+ const passwordHash = await hash(data.password, salt);
if (!passwordHash) {
throw new Error('Failed to hash password.');
}
@@ -23,21 +60,29 @@ export async function registerUser(data) {
});
}
-export async function deleteUser(id, password) {
- const user = await DbUser.findById(id);
- DbUser.exists({ _id: id }).then(exists => {
- if (!exists) {
- throw new Error('User does not exist.');
- }
+export async function deleteUser(data) {
+ var user = await getUserByAuth(data);
+
+ await DbUser.findByIdAndDelete(data._id);
+}
+
+/**
+ * @param data {AuthDto}
+ * @param deviceName {string}
+ * @returns {Promise<WhoAmIDto>}
+ */
+export async function loginUser(data, deviceName) {
+ const user = await getUserByAuth(data);
+ const device = await user.devices.create({
+ name: deviceName
});
- if (!user) {
- throw new Error('User not found.');
- }
- const isPasswordValid = await compare(password, user.passwordHash);
- if (!isPasswordValid) {
- throw new Error('Invalid password.');
- }
+ user.devices.push(device);
+ await user.save();
- await DbUser.findByIdAndDelete(id);
+ return WhoAmIDto.create({
+ userId: user._id,
+ username: user.username,
+ deviceId: device._id
+ });
}
diff --git a/src/db/schemas/user.js b/src/db/schemas/user.js
index 9d08680..f490966 100644
--- a/src/db/schemas/user.js
+++ b/src/db/schemas/user.js
@@ -1,6 +1,24 @@
import { model, Schema } from 'mongoose';
import { hash, compare } from 'bcrypt';
+export const deviceSchema = new Schema({
+ name: {
+ type: String,
+ required: true,
+ trim: true
+ },
+ createdAt: {
+ type: Date,
+ default: Date.now,
+ immutable: true
+ },
+ lastSeen: {
+ type: Date,
+ default: Date.now,
+ required: true
+ }
+});
+
/**
* User schema for MongoDB.
* @type {module:mongoose.Schema}
@@ -31,6 +49,10 @@ export const userSchema = new Schema({
type: Date,
default: Date.now,
immutable: true
+ },
+ devices: {
+ type: [deviceSchema],
+ default: []
}
});
diff --git a/src/dto/auth/LoginDto.js b/src/dto/auth/AuthDto.js
index b675b4d..14e09ae 100644
--- a/src/dto/auth/LoginDto.js
+++ b/src/dto/auth/AuthDto.js
@@ -1,7 +1,10 @@
import { SafeNSoundError } from '#util/error.js';
import Joi from 'joi';
-export class LoginDto {
+/**
+ * Generic authentication DTO.
+ */
+export class AuthDto {
static schema = new Joi.object({
username: Joi.string().required(),
email: Joi.string().email().required(),
@@ -9,16 +12,19 @@ export class LoginDto {
}).or('username', 'email');
username;
+ email;
password;
- async Create({ username, email, password }) {
- this.username = username ?? email;
- this.password = password;
+ static async create(data) {
+ const obj = new AuthDto();
+ for (const key of Object.keys(data)) {
+ if (key in obj) {
+ obj[key] = data[key];
+ }
+ }
try {
- return await LoginDto.schema.validateAsync(this, {
- abortEarly: true
- });
+ return await AuthDto.schema.validateAsync(obj);
} catch (e) {
console.log(e);
throw new SafeNSoundError({
diff --git a/src/dto/auth/DeviceDto.js b/src/dto/auth/DeviceDto.js
new file mode 100644
index 0000000..40f1959
--- /dev/null
+++ b/src/dto/auth/DeviceDto.js
@@ -0,0 +1,36 @@
+import { SafeNSoundError } from '#util/error.js';
+import Joi from 'joi';
+
+export class RegisterDto {
+ static schema = new Joi.object({
+ username: Joi.string().required(),
+ email: Joi.string().email().required(),
+ password: Joi.string().required(),
+ type: Joi.string().valid('user', 'monitor', 'admin').required()
+ });
+
+ username;
+ email;
+ password;
+ type = 'user';
+
+ static async create(data) {
+ const obj = new RegisterDto();
+ for (const key of Object.keys(data)) {
+ if (key in obj) {
+ obj[key] = data[key];
+ }
+ }
+
+ try {
+ return await RegisterDto.schema.validateAsync(obj);
+ } catch (e) {
+ console.log(e);
+ throw new SafeNSoundError({
+ errCode: 'JOI_VALIDATION_ERROR',
+ message: e.message,
+ validation_details: e.details
+ });
+ }
+ }
+}
diff --git a/src/dto/auth/WhoAmIDto.js b/src/dto/auth/WhoAmIDto.js
new file mode 100644
index 0000000..ae1795a
--- /dev/null
+++ b/src/dto/auth/WhoAmIDto.js
@@ -0,0 +1,26 @@
+import { SafeNSoundError } from '#util/error.js';
+import Joi from 'joi';
+
+/**
+ * Generic authentication DTO.
+ */
+export class WhoAmIDto {
+ userId;
+ username;
+ deviceId;
+
+ /**
+ * @param data {WhoAmIDto}
+ * @returns {Promise<WhoAmIDto>}
+ */
+ static async create(data) {
+ const obj = new WhoAmIDto();
+ for (const key of Object.keys(data)) {
+ if (key in obj) {
+ obj[key] = data[key];
+ }
+ }
+
+ return obj;
+ }
+}
diff --git a/src/dto/auth/index.js b/src/dto/auth/index.js
index 6d57d5e..aa1d435 100644
--- a/src/dto/auth/index.js
+++ b/src/dto/auth/index.js
@@ -1,2 +1,2 @@
-export * from './LoginDto.js';
+export * from './AuthDto.js';
export * from './RegisterDto.js';
diff --git a/src/util/error.js b/src/util/error.js
index 5fc454a..9816b65 100644
--- a/src/util/error.js
+++ b/src/util/error.js
@@ -1,4 +1,7 @@
export class SafeNSoundError extends Error {
+ /**
+ * @param options {SafeNSoundError}
+ */
constructor(options) {
super();
if (typeof options === 'string') {
|