diff --git a/src/lib/server/api/index.ts b/src/lib/server/api/index.ts index de5dab3..257aea9 100644 --- a/src/lib/server/api/index.ts +++ b/src/lib/server/api/index.ts @@ -6,6 +6,7 @@ import { container } from 'tsyringe'; import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware'; import { IamController } from './controllers/iam.controller'; import { config } from './common/config'; +import { UsersController } from './controllers/users.controller'; /* -------------------------------------------------------------------------- */ /* Client Request */ @@ -36,7 +37,6 @@ app.use(verifyOrigin).use(validateAuthSession); const routes = app .route('/iam', container.resolve(IamController).routes()) - /* -------------------------------------------------------------------------- */ /* Exports */ /* -------------------------------------------------------------------------- */ diff --git a/src/lib/server/api/mockTest.ts b/src/lib/server/api/mockTest.ts new file mode 100644 index 0000000..15a9d1b --- /dev/null +++ b/src/lib/server/api/mockTest.ts @@ -0,0 +1,11 @@ +import { Scrypt } from "oslo/password"; + + +export async function hash(value: string) { + const scrypt = new Scrypt() + return scrypt.hash(value); +} + +export function verify(hashedValue: string, value: string) { + return new Scrypt().verify(hashedValue, value); +} diff --git a/src/lib/server/api/repositories/email-verifications.repository.ts b/src/lib/server/api/repositories/email-verifications.repository.ts index 5161be1..2ec7ca2 100644 --- a/src/lib/server/api/repositories/email-verifications.repository.ts +++ b/src/lib/server/api/repositories/email-verifications.repository.ts @@ -1,6 +1,6 @@ import { inject, injectable } from "tsyringe"; import { DatabaseProvider } from "../providers"; -import { and, eq, lte, type InferInsertModel } from "drizzle-orm"; +import { and, eq, gte, lte, type InferInsertModel } from "drizzle-orm"; import type { Repository } from "../interfaces/repository.interface"; import { takeFirst, takeFirstOrThrow } from "../infrastructure/database/utils"; import { emailVerificationsTable } from "../infrastructure/database/tables/email-verifications.table"; @@ -24,7 +24,7 @@ export class EmailVerificationsRepository implements Repository { return this.db.select().from(emailVerificationsTable).where( and( eq(emailVerificationsTable.userId, userId), - lte(emailVerificationsTable.expiresAt, new Date()) + gte(emailVerificationsTable.expiresAt, new Date()) )).then(takeFirst) } diff --git a/src/lib/server/api/repositories/login-requests.repository.ts b/src/lib/server/api/repositories/login-requests.repository.ts index 0a1d3f0..027555c 100644 --- a/src/lib/server/api/repositories/login-requests.repository.ts +++ b/src/lib/server/api/repositories/login-requests.repository.ts @@ -2,7 +2,7 @@ import { inject, injectable } from "tsyringe"; import { DatabaseProvider } from "../providers"; import type { Repository } from "../interfaces/repository.interface"; import { and, eq, gte, type InferInsertModel } from "drizzle-orm"; -import { takeFirst } from "../infrastructure/database/utils"; +import { takeFirst, takeFirstOrThrow } from "../infrastructure/database/utils"; import { loginRequestsTable } from "../infrastructure/database/tables/login-requests.table"; export type CreateLoginRequest = Pick, 'email' | 'expiresAt' | 'hashedToken'>; @@ -15,7 +15,7 @@ export class LoginRequestsRepository implements Repository { return this.db.insert(loginRequestsTable).values(data).onConflictDoUpdate({ target: loginRequestsTable.email, set: data - }) + }).returning().then(takeFirstOrThrow) } async findOneByEmail(email: string) { diff --git a/src/lib/server/api/services/login-requests.service.ts b/src/lib/server/api/services/login-requests.service.ts index 1e4add2..a4b5dfa 100644 --- a/src/lib/server/api/services/login-requests.service.ts +++ b/src/lib/server/api/services/login-requests.service.ts @@ -17,16 +17,16 @@ export class LoginRequestsService { @inject(TokensService) private readonly tokensService: TokensService, @inject(MailerService) private readonly mailerService: MailerService, @inject(UsersRepository) private readonly usersRepository: UsersRepository, - @inject(LoginRequestsRepository) private readonly loginRequetsRepository: LoginRequestsRepository, + @inject(LoginRequestsRepository) private readonly loginRequestsRepository: LoginRequestsRepository, ) { } async create(data: RegisterEmailDto) { // generate a token, expiry date, and hash const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm'); // save the login request to the database - ensuring we save the hashedToken - await this.loginRequetsRepository.create({ email: data.email, hashedToken, expiresAt: expiry }); + await this.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry }); // send the login request email - this.mailerService.sendLoginRequest({ + await this.mailerService.sendLoginRequest({ to: data.email, props: { token: token } }); @@ -57,7 +57,7 @@ export class LoginRequestsService { private async fetchValidRequest(email: string, token: string) { return await this.db.transaction(async (trx) => { // fetch the login request - const loginRequest = await this.loginRequetsRepository.trxHost(trx).findOneByEmail(email) + const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email) if (!loginRequest) return null; // check if the token is valid @@ -65,7 +65,7 @@ export class LoginRequestsService { if (!isValidRequest) return null // if the token is valid, burn the request - await this.loginRequetsRepository.trxHost(trx).deleteById(loginRequest.id); + await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id); return loginRequest }) } diff --git a/src/lib/server/api/tests/login-requests.service.test.ts b/src/lib/server/api/tests/login-requests.service.test.ts new file mode 100644 index 0000000..cc3fbf1 --- /dev/null +++ b/src/lib/server/api/tests/login-requests.service.test.ts @@ -0,0 +1,72 @@ +import 'reflect-metadata'; +import { LoginRequestsService } from '../services/login-requests.service'; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; +import { TokensService } from '../services/tokens.service'; +import { MailerService } from '../services/mailer.service'; +import { UsersRepository } from '../repositories/users.repository'; +import { DatabaseProvider, LuciaProvider } from '../providers'; +import { LoginRequestsRepository } from '../repositories/login-requests.repository'; +import { PgDatabase } from 'drizzle-orm/pg-core'; +import { container } from 'tsyringe'; + +describe('LoginRequestService', () => { + let service: LoginRequestsService; + let tokensService = vi.mocked(TokensService.prototype) + let mailerService = vi.mocked(MailerService.prototype); + let usersRepository = vi.mocked(UsersRepository.prototype); + let loginRequestsRepository = vi.mocked(LoginRequestsRepository.prototype); + let luciaProvider = vi.mocked(LuciaProvider); + let databaseProvider = vi.mocked(PgDatabase); + + beforeAll(() => { + service = container + .register(TokensService, { useValue: tokensService }) + .register(MailerService, { useValue: mailerService }) + .register(UsersRepository, { useValue: usersRepository }) + .register(LoginRequestsRepository, { useValue: loginRequestsRepository }) + .register(LuciaProvider, { useValue: luciaProvider }) + .register(DatabaseProvider, { useValue: databaseProvider }) + .resolve(LoginRequestsService); + }); + + + afterAll(() => { + vi.resetAllMocks() + }) + + describe('Create', () => { + tokensService.generateTokenWithExpiryAndHash = vi.fn().mockResolvedValue({ + token: "111", + expiry: new Date(), + hashedToken: "111" + } satisfies Awaited>) + + loginRequestsRepository.create = vi.fn().mockResolvedValue({ + createdAt: new Date(), + email: 'me@test.com', + expiresAt: new Date(), + hashedToken: '111', + id: '1', + updatedAt: new Date() + } satisfies Awaited>) + + mailerService.sendLoginRequest = vi.fn().mockResolvedValue(null) + + const spy_mailerService_sendLoginRequest = vi.spyOn(mailerService, 'sendLoginRequest') + const spy_tokensService_generateTokenWithExpiryAndHash = vi.spyOn(tokensService, 'generateTokenWithExpiryAndHash') + const spy_loginRequestsRepository_create = vi.spyOn(loginRequestsRepository, 'create') + + it('should resolve', async () => { + await expect(service.create({ email: "test" })).resolves.toBeUndefined() + }) + it('should generate a token with expiry and hash', async () => { + expect(spy_tokensService_generateTokenWithExpiryAndHash).toBeCalledTimes(1) + }) + it('should send an email with token', async () => { + expect(spy_mailerService_sendLoginRequest).toHaveBeenCalledTimes(1) + }) + it('should create a new login request record', async () => { + expect(spy_loginRequestsRepository_create).toBeCalledTimes(1) + }) + }) +}); diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5364739 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,3 @@ +{ + "status": "failed" +} \ No newline at end of file