diff --git a/src/db/dbAccess/index.js b/src/db/dbAccess/index.js
new file mode 100644
index 0000000..ee1c337
--- /dev/null
+++ b/src/db/dbAccess/index.js
@@ -0,0 +1 @@
+export * from './user.js';
diff --git a/src/db/dbAccess/user.js b/src/db/dbAccess/user.js
new file mode 100644
index 0000000..413b1cf
--- /dev/null
+++ b/src/db/dbAccess/user.js
@@ -0,0 +1,41 @@
+import { hash, compare } from 'bcrypt';
+import { DbUser } from '#db/schemas/index.js';
+
+export async function registerUser(username, password, email, type = 'user') {
+ if (!username || !password || !email) {
+ throw new Error(
+ 'Username, password, and email are required to register a user.'
+ );
+ }
+
+ const passwordHash = await hash(password, 10);
+ if (!passwordHash) {
+ throw new Error('Failed to hash password.');
+ }
+
+ return DbUser.create({
+ username,
+ passwordHash,
+ email,
+ type
+ });
+}
+
+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.');
+ }
+ });
+ if (!user) {
+ throw new Error('User not found.');
+ }
+
+ const isPasswordValid = await compare(password, user.passwordHash);
+ if (!isPasswordValid) {
+ throw new Error('Invalid password.');
+ }
+
+ await DbUser.findByIdAndDelete(id);
+}
diff --git a/src/db/dbAccess/user.test.js b/src/db/dbAccess/user.test.js
new file mode 100644
index 0000000..e8e1af5
--- /dev/null
+++ b/src/db/dbAccess/user.test.js
@@ -0,0 +1,44 @@
+import * as dotenv from 'dotenv';
+import { it } from 'node:test';
+import { deleteUser, registerUser } from '#db/index.js';
+import * as assert from 'node:assert';
+import { initDb } from '#db/db.js';
+import { disconnect } from 'mongoose';
+
+dotenv.config();
+
+await initDb();
+
+async function createTestUser() {
+ const username = (Math.random() * 1000000).toString();
+ const password = (Math.random() * 1000000).toString();
+ const email = (Math.random() * 1000000).toString() + '@example.com';
+ return {
+ username,
+ password,
+ email,
+ user: await registerUser(username, password, email)
+ };
+}
+
+await it('Can create user', async () => {
+ await assert.doesNotReject(createTestUser());
+});
+
+await it('Can delete user', async () => {
+ const { username, password, email, user } = await createTestUser();
+
+ const deletePromise = deleteUser(user._id, password);
+ await assert.doesNotReject(deletePromise);
+});
+
+await it('Cant delete nonexistant user', async () => {
+ await assert.rejects(deleteUser('abc', ''));
+});
+
+await it('Cant delete user with invalid password', async () => {
+ const user = await createTestUser();
+ await assert.rejects(deleteUser(user.user._id, 'wrongpassword'));
+});
+
+await disconnect();
diff --git a/src/db/index.js b/src/db/index.js
index cd306b7..0680880 100644
--- a/src/db/index.js
+++ b/src/db/index.js
@@ -1,2 +1,3 @@
export * from './db.js';
export * from './schemas/index.js';
+export * from './dbAccess/index.js';
diff --git a/src/db/schemas/user.js b/src/db/schemas/user.js
index 1a7e048..9d08680 100644
--- a/src/db/schemas/user.js
+++ b/src/db/schemas/user.js
@@ -1,4 +1,5 @@
import { model, Schema } from 'mongoose';
+import { hash, compare } from 'bcrypt';
/**
* User schema for MongoDB.
diff --git a/src/util/error.js b/src/util/error.js
new file mode 100644
index 0000000..96f1deb
--- /dev/null
+++ b/src/util/error.js
@@ -0,0 +1,36 @@
+export class SafeNSoundError extends Error {
+ constructor(options) {
+ super();
+ if (typeof options === 'string') {
+ this.errCode = options;
+ super.message = this.getDefaultMessage();
+ } else if (typeof options === 'object') {
+ this.errCode = options.errCode || 'UNKNOWN_ERROR';
+ super.message = options.message || this.getDefaultMessage();
+ } else {
+ this.errCode = 'UNKNOWN_ERROR';
+ this.message =
+ 'An unknown error occurred (invalid SafeNSoundError constructor options)';
+ }
+ }
+
+ getDefaultMessage() {
+ switch (this.type) {
+ case 'USER_NOT_FOUND':
+ return 'User not found';
+ case 'INVALID_CREDENTIALS':
+ return 'Invalid credentials';
+ case 'EMAIL_ALREADY_EXISTS':
+ return 'Email already exists';
+ case 'PASSWORD_TOO_WEAK':
+ return 'Password is too weak';
+ case 'TOKEN_EXPIRED':
+ return 'Token has expired';
+ case 'UNAUTHORIZED':
+ return 'Unauthorized access';
+ case 'UNKNOWN_ERROR':
+ default:
+ return 'An unknown error occurred';
+ }
+ }
+}
diff --git a/src/util/jwtUtils.js b/src/util/jwtUtils.js
index 115c9c5..9031631 100644
--- a/src/util/jwtUtils.js
+++ b/src/util/jwtUtils.js
@@ -1,6 +1,7 @@
import {existsSync} from 'fs';
import {readFile, writeFile} from "node:fs/promises";
import {generateKeyPairSync, createHash, createPublicKey, createPrivateKey} from 'node:crypto';
+import jwt from "jsonwebtoken";
let privateKey, publicKey, fingerprint;
@@ -10,6 +11,8 @@ export async function initJwt() {
throw new Error('JWT secret path is not defined in environment variables, or the directory does not exist.');
}
+ console.log(`[JWT] Initializing JWT with secret path: ${secretPath}`);
+
const privateKeyPath = `${secretPath}/jwt.key`;
const publicKeyPath = `${secretPath}/jwt.key.pub`;
@@ -55,9 +58,40 @@ const jwtOptions = {
}
export async function generateJwtToken(user) {
+ if (!privateKey) {
+ throw new Error('JWT private key is not initialized. Please call initJwt() first.');
+ }
+
+ const payload = {
+ sub: user._id.toString(),
+ username: user.username,
+ type: user.type,
+ iat: Math.floor(Date.now() / 1000)
+ };
+ return new Promise((resolve, reject) => {
+ jwt.sign(payload, privateKey, jwtOptions, (err, token) => {
+ if (err) {
+ console.error('[JWT] Error generating token:', err);
+ return reject(err);
+ }
+ resolve(token);
+ });
+ });
}
export async function validateJwtToken(token) {
+ if (!publicKey) {
+ throw new Error('JWT public key is not initialized. Please call initJwt() first.');
+ }
+ return new Promise((resolve, reject) => {
+ jwt.verify(token, publicKey, jwtOptions, (err, decoded) => {
+ if (err) {
+ console.error('[JWT] Token validation failed:', err);
+ return reject(err);
+ }
+ resolve(decoded);
+ });
+ });
}
\ No newline at end of file
diff --git a/src/util/jwtUtils.test.js b/src/util/jwtUtils.test.js
new file mode 100644
index 0000000..94a7d99
--- /dev/null
+++ b/src/util/jwtUtils.test.js
@@ -0,0 +1,35 @@
+import { it } from 'node:test';
+import { initJwt, generateJwtToken, validateJwtToken } from './jwtUtils.js';
+import * as dotenv from 'dotenv';
+import { tmpdir } from 'node:os';
+import { mkdtemp } from 'node:fs/promises';
+import * as assert from 'node:assert';
+dotenv.config();
+
+const user = {
+ _id: 'meow',
+ username: 'testuser'
+};
+
+await it('Should be able to generate new secrets', async () => {
+ process.env.JWT_SECRET_PATH = await mkdtemp(tmpdir());
+ await initJwt();
+});
+
+await it('Should be able to load the JWT utils', async () => {
+ process.env.JWT_SECRET_PATH = await mkdtemp(tmpdir());
+ await initJwt();
+ await initJwt();
+});
+
+it('Should be able to create a JWT token', async () => {
+ await initJwt();
+ await generateJwtToken(user);
+});
+
+it('Should be able to validate a JWT token', async () => {
+ await initJwt();
+ const token = await generateJwtToken(user);
+ const result = await validateJwtToken(token);
+ console.log(result);
+});
diff --git a/src/util/secretUtils.js b/src/util/secretUtils.js
index 92e1b1c..7394395 100644
--- a/src/util/secretUtils.js
+++ b/src/util/secretUtils.js
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
export async function readSecret(name, path) {
console.log(`[SECRET] Reading secret "${name}" from path: ${path}`);
if (!path) {
- throw new Error('Path to secret file is required');
+ throw new Error(`Path to secret file is required: ${name}`);
}
const content = await fs.readFile(path, 'utf8');
return content.trim();
diff --git a/src/util/secretUtils.test.js b/src/util/secretUtils.test.js
new file mode 100644
index 0000000..d95d56f
--- /dev/null
+++ b/src/util/secretUtils.test.js
@@ -0,0 +1,23 @@
+import { it } from 'node:test';
+import * as dotenv from 'dotenv';
+import { tmpdir } from 'node:os';
+import * as assert from 'node:assert';
+import { readSecret } from '#util/secretUtils.js';
+import { writeFile, mkdtemp } from 'node:fs/promises';
+
+dotenv.config();
+
+await it('Should fail on nonexistant secret', async () => {
+ const secretPath = (await mkdtemp(tmpdir())) + '/secret';
+ await assert.rejects(readSecret('Nonexistant secret', secretPath));
+});
+
+await it('Should read secret from file', async () => {
+ const secretPath = (await mkdtemp(tmpdir())) + '/secret';
+ await writeFile(secretPath, 'testsecret');
+ await assert.doesNotReject(readSecret('Test secret', secretPath));
+ await assert.equal(
+ await readSecret('Test secret', secretPath),
+ 'testsecret'
+ );
+});
|