From ebc752233ce197d500c1211467f3736c0d544ad4 Mon Sep 17 00:00:00 2001 From: rykuno Date: Wed, 7 Aug 2024 10:19:21 -0500 Subject: [PATCH] combined all iam services --- .../server/api/controllers/iam.controller.ts | 17 ++--- .../services/email-verifications.service.ts | 67 ----------------- src/lib/server/api/services/iam.service.ts | 57 ++++++++++++++- .../api/services/login-requests.service.ts | 72 ------------------- 4 files changed, 58 insertions(+), 155 deletions(-) delete mode 100644 src/lib/server/api/services/email-verifications.service.ts delete mode 100644 src/lib/server/api/services/login-requests.service.ts diff --git a/src/lib/server/api/controllers/iam.controller.ts b/src/lib/server/api/controllers/iam.controller.ts index d149acd..0940ebe 100644 --- a/src/lib/server/api/controllers/iam.controller.ts +++ b/src/lib/server/api/controllers/iam.controller.ts @@ -8,13 +8,10 @@ import { signInEmailDto } from '../../../dtos/signin-email.dto'; import { updateEmailDto } from '../../../dtos/update-email.dto'; import { verifyEmailDto } from '../../../dtos/verify-email.dto'; import { registerEmailDto } from '../../../dtos/register-email.dto'; -import { EmailVerificationsService } from '../services/email-verifications.service'; -import { LoginRequestsService } from '../services/login-requests.service'; import type { HonoTypes } from '../common/types/hono.type'; import type { Controller } from '../common/inferfaces/controller.interface'; import { limiter } from '../middlewares/rate-limiter.middlware'; import { requireAuth } from '../middlewares/auth.middleware'; -import TestJob from '../jobs/test.job'; @injectable() export class IamController implements Controller { @@ -22,29 +19,23 @@ export class IamController implements Controller { constructor( @inject(IamService) private iamService: IamService, - @inject(LoginRequestsService) private loginRequestsService: LoginRequestsService, - @inject(EmailVerificationsService) private emailVerificationsService: EmailVerificationsService, @inject(LuciaProvider) private lucia: LuciaProvider, - @inject(TestJob) private testJob: TestJob ) { } routes() { return this.controller .get('/user', async (c) => { const user = c.var.user; - console.log('uwu') - this.testJob.queue('green'); - // this.testJob.worker(); return c.json({ user: user }); }) .post('/login/request', zValidator('json', registerEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { const { email } = c.req.valid('json'); - await this.loginRequestsService.create({ email }); + await this.iamService.createLoginRequest({ email }); return c.json({ message: 'Verification email sent' }); }) .post('/login/verify', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { const { email, token } = c.req.valid('json'); - const session = await this.loginRequestsService.verify({ email, token }); + const session = await this.iamService.verifyLoginRequest({ email, token }); const sessionCookie = this.lucia.createSessionCookie(session.id); setCookie(c, sessionCookie.name, sessionCookie.value, { path: sessionCookie.attributes.path, @@ -74,14 +65,14 @@ export class IamController implements Controller { }) .patch('/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { const json = c.req.valid('json'); - await this.emailVerificationsService.dispatchEmailVerificationRequest(c.var.user.id, json.email); + await this.iamService.dispatchEmailVerificationRequest(c.var.user.id, json.email); return c.json({ message: 'Verification email sent' }); }) // this could also be named to use custom methods, aka /email#verify // https://cloud.google.com/apis/design/custom_methods .post('/email/verification', requireAuth, zValidator('json', verifyEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { const json = c.req.valid('json'); - await this.emailVerificationsService.processEmailVerificationRequest(c.var.user.id, json.token); + await this.iamService.processEmailVerificationRequest(c.var.user.id, json.token); return c.json({ message: 'Verified and updated' }); }); } diff --git a/src/lib/server/api/services/email-verifications.service.ts b/src/lib/server/api/services/email-verifications.service.ts deleted file mode 100644 index 59c4d76..0000000 --- a/src/lib/server/api/services/email-verifications.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { inject, injectable } from 'tsyringe'; -import { MailerService } from './mailer.service'; -import { TokensService } from './tokens.service'; -import { UsersRepository } from '../repositories/users.repository'; -import { EmailVerificationsRepository } from '../repositories/email-verifications.repository'; -import { DatabaseProvider } from '../providers/database.provider'; -import { EmailChangeNoticeEmail } from '../emails/email-change-notice.email'; -import { LoginVerificationEmail } from '../emails/login-verification.email'; -import { BadRequest } from '../common/exceptions'; - -@injectable() -export class EmailVerificationsService { - constructor( - @inject(DatabaseProvider) private readonly db: DatabaseProvider, - @inject(TokensService) private readonly tokensService: TokensService, - @inject(MailerService) private readonly mailerService: MailerService, - @inject(UsersRepository) private readonly usersRepository: UsersRepository, - @inject(EmailVerificationsRepository) private readonly emailVerificationsRepository: EmailVerificationsRepository, - ) { } - - // These steps follow the process outlined in OWASP's "Changing A User's Email Address" guide. - // https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#changing-a-users-registered-email-address - async dispatchEmailVerificationRequest(userId: string, requestedEmail: string) { - // generate a token and expiry - const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm') - const user = await this.usersRepository.findOneByIdOrThrow(userId) - - // create a new email verification record - await this.emailVerificationsRepository.create({ requestedEmail, userId, hashedToken, expiresAt: expiry }) - - // A confirmation-required email message to the proposed new address, instructing the user to - // confirm the change and providing a link for unexpected situations - this.mailerService.send({ - to: requestedEmail, - email: new LoginVerificationEmail(token) - }) - - // A notification-only email message to the current address, alerting the user to the impending change and - // providing a link for an unexpected situation. - this.mailerService.send({ - to: user.email, - email: new EmailChangeNoticeEmail() - }) - } - - async processEmailVerificationRequest(userId: string, token: string) { - const validRecord = await this.findAndBurnEmailVerificationToken(userId, token) - if (!validRecord) throw BadRequest('Invalid token'); - await this.usersRepository.update(userId, { email: validRecord.requestedEmail, verified: true }); - } - - private async findAndBurnEmailVerificationToken(userId: string, token: string) { - return this.db.transaction(async (trx) => { - // find a valid record - const emailVerificationRecord = await this.emailVerificationsRepository.trxHost(trx).findValidRecord(userId); - if (!emailVerificationRecord) return null; - - // check if the token is valid - const isValidRecord = await this.tokensService.verifyHashedToken(emailVerificationRecord.hashedToken, token); - if (!isValidRecord) return null - - // burn the token if it is valid - await this.emailVerificationsRepository.trxHost(trx).deleteById(emailVerificationRecord.id) - return emailVerificationRecord - }) - } -} diff --git a/src/lib/server/api/services/iam.service.ts b/src/lib/server/api/services/iam.service.ts index acc2064..35bce7d 100644 --- a/src/lib/server/api/services/iam.service.ts +++ b/src/lib/server/api/services/iam.service.ts @@ -10,6 +10,8 @@ import { LoginVerificationEmail } from '../emails/login-verification.email'; import { DatabaseProvider } from '../providers/database.provider'; import { BadRequest } from '../common/exceptions'; import { WelcomeEmail } from '../emails/welcome.email'; +import { EmailVerificationsRepository } from '../repositories/email-verifications.repository'; +import { EmailChangeNoticeEmail } from '../emails/email-change-notice.email'; @injectable() export class IamService { @@ -20,7 +22,7 @@ export class IamService { @inject(MailerService) private readonly mailerService: MailerService, @inject(UsersRepository) private readonly usersRepository: UsersRepository, @inject(LoginRequestsRepository) private readonly loginRequestsRepository: LoginRequestsRepository, - + @inject(EmailVerificationsRepository) private readonly emailVerificationsRepository: EmailVerificationsRepository, ) { } async createLoginRequest(data: RegisterEmailDto) { @@ -46,6 +48,41 @@ export class IamService { return this.lucia.createSession(existingUser.id, {}); } + // These steps follow the process outlined in OWASP's "Changing A User's Email Address" guide. + // https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#changing-a-users-registered-email-address + async dispatchEmailVerificationRequest(userId: string, requestedEmail: string) { + // generate a token and expiry + const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm') + const user = await this.usersRepository.findOneByIdOrThrow(userId) + + // create a new email verification record + await this.emailVerificationsRepository.create({ requestedEmail, userId, hashedToken, expiresAt: expiry }) + + // A confirmation-required email message to the proposed new address, instructing the user to + // confirm the change and providing a link for unexpected situations + this.mailerService.send({ + to: requestedEmail, + email: new LoginVerificationEmail(token) + }) + + // A notification-only email message to the current address, alerting the user to the impending change and + // providing a link for an unexpected situation. + this.mailerService.send({ + to: user.email, + email: new EmailChangeNoticeEmail() + }) + } + + async processEmailVerificationRequest(userId: string, token: string) { + const validRecord = await this.findAndBurnEmailVerificationToken(userId, token) + if (!validRecord) throw BadRequest('Invalid token'); + await this.usersRepository.update(userId, { email: validRecord.requestedEmail, verified: true }); + } + + async logout(sessionId: string) { + return this.lucia.invalidateSession(sessionId); + } + // Create a new user and send a welcome email - or other onboarding process private async handleNewUserRegistration(email: string) { const newUser = await this.usersRepository.create({ email, verified: true }) @@ -71,7 +108,21 @@ export class IamService { }) } - async logout(sessionId: string) { - return this.lucia.invalidateSession(sessionId); + private async findAndBurnEmailVerificationToken(userId: string, token: string) { + return this.db.transaction(async (trx) => { + // find a valid record + const emailVerificationRecord = await this.emailVerificationsRepository.trxHost(trx).findValidRecord(userId); + if (!emailVerificationRecord) return null; + + // check if the token is valid + const isValidRecord = await this.tokensService.verifyHashedToken(emailVerificationRecord.hashedToken, token); + if (!isValidRecord) return null + + // burn the token if it is valid + await this.emailVerificationsRepository.trxHost(trx).deleteById(emailVerificationRecord.id) + return emailVerificationRecord + }) } + + } diff --git a/src/lib/server/api/services/login-requests.service.ts b/src/lib/server/api/services/login-requests.service.ts deleted file mode 100644 index 432b8ff..0000000 --- a/src/lib/server/api/services/login-requests.service.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { inject, injectable } from 'tsyringe'; -import { MailerService } from './mailer.service'; -import { TokensService } from './tokens.service'; -import { LuciaProvider } from '../providers/lucia.provider'; -import { UsersRepository } from '../repositories/users.repository'; -import type { SignInEmailDto } from '../dtos/signin-email.dto'; -import type { RegisterEmailDto } from '../dtos/register-email.dto'; -import { LoginRequestsRepository } from '../repositories/login-requests.repository'; -import { LoginVerificationEmail } from '../emails/login-verification.email'; -import { DatabaseProvider } from '../providers/database.provider'; -import { BadRequest } from '../common/exceptions'; -import { WelcomeEmail } from '../emails/welcome.email'; - -@injectable() -export class LoginRequestsService { - constructor( - @inject(LuciaProvider) private readonly lucia: LuciaProvider, - @inject(DatabaseProvider) private readonly db: DatabaseProvider, - @inject(TokensService) private readonly tokensService: TokensService, - @inject(MailerService) private readonly mailerService: MailerService, - @inject(UsersRepository) private readonly usersRepository: UsersRepository, - @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.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry }); - // send the login request email - await this.mailerService.send({ email: new LoginVerificationEmail(token), to: data.email }); - } - - async verify(data: SignInEmailDto) { - const validLoginRequest = await this.fetchValidRequest(data.email, data.token); - if (!validLoginRequest) throw BadRequest('Invalid token'); - - let existingUser = await this.usersRepository.findOneByEmail(data.email); - - if (!existingUser) { - const newUser = await this.handleNewUserRegistration(data.email); - return this.lucia.createSession(newUser.id, {}); - } - - return this.lucia.createSession(existingUser.id, {}); - } - - // Create a new user and send a welcome email - or other onboarding process - private async handleNewUserRegistration(email: string) { - const newUser = await this.usersRepository.create({ email, verified: true }) - await this.mailerService.send({ email: new WelcomeEmail(), to: newUser.email }); - // TODO: add whatever onboarding process or extra data you need here - return newUser - } - - // Fetch a valid request from the database, verify the token and burn the request if it is valid - private async fetchValidRequest(email: string, token: string) { - return await this.db.transaction(async (trx) => { - // fetch the login request - const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email) - if (!loginRequest) return null; - - // check if the token is valid - const isValidRequest = await this.tokensService.verifyHashedToken(loginRequest.hashedToken, token); - if (!isValidRequest) return null - - // if the token is valid, burn the request - await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id); - return loginRequest - }) - } -} \ No newline at end of file