mirror of
https://github.com/BradNut/TofuStack
synced 2025-09-08 17:40:26 +00:00
combined all iam services
This commit is contained in:
parent
a2a3f3faf3
commit
ebc752233c
4 changed files with 58 additions and 155 deletions
|
|
@ -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' });
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue