mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Adding verify login with TOTP credentials coming from the credentials table.
This commit is contained in:
parent
bf55b04de6
commit
dbdac430ef
6 changed files with 99 additions and 29 deletions
|
|
@ -19,7 +19,7 @@ export class LoginController implements Controller {
|
|||
return this.controller
|
||||
.post('/', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const { username, password } = c.req.valid('json');
|
||||
await this.loginRequestsService.verify({ username, password });
|
||||
await this.loginRequestsService.verify({ username, password }, c.req);
|
||||
return c.json({ message: 'Verification email sent' });
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,22 @@
|
|||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { inject, injectable } from 'tsyringe';
|
||||
import { requireAuth } from "../middleware/auth.middleware";
|
||||
import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto';
|
||||
import { limiter } from '../middleware/rate-limiter.middleware';
|
||||
import type { HonoTypes } from '../types';
|
||||
import type { Controller } from '../interfaces/controller.interface';
|
||||
|
||||
const app = new Hono()
|
||||
@injectable()
|
||||
export class UserController implements Controller {
|
||||
controller = new Hono<HonoTypes>();
|
||||
|
||||
constructor(
|
||||
@inject('LoginRequestsService') private readonly loginRequestsService: LoginRequestsService
|
||||
) { }
|
||||
|
||||
routes() {
|
||||
return this.controller
|
||||
.get('/me', requireAuth, async (c) => {
|
||||
const user = c.var.user;
|
||||
return c.json({ user });
|
||||
|
|
@ -18,5 +30,5 @@ const app = new Hono()
|
|||
await this.loginRequestsService.create({ email });
|
||||
return c.json({ message: 'Verification email sent' });
|
||||
});
|
||||
|
||||
export default app;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { pgTable, text, uuid } from "drizzle-orm/pg-core";
|
|||
import { timestamps } from '../utils';
|
||||
import { usersTable } from "./users.table";
|
||||
|
||||
enum CredentialsType {
|
||||
export enum CredentialsType {
|
||||
SECRET = 'secret',
|
||||
PASSWORD = 'password',
|
||||
TOTP = 'totp',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { eq, type InferInsertModel } from "drizzle-orm";
|
||||
import { credentialsTable } from "../infrastructure/database/tables/credentials.table";
|
||||
import { and, eq, type InferInsertModel } from "drizzle-orm";
|
||||
import { credentialsTable, CredentialsType } from "../infrastructure/database/tables/credentials.table";
|
||||
import { db } from "../infrastructure/database";
|
||||
import { takeFirstOrThrow } from "../infrastructure/database/utils";
|
||||
|
||||
|
|
@ -8,6 +8,30 @@ export type UpdateCredentials = Partial<CreateCredentials>;
|
|||
|
||||
export class CredentialsRepository {
|
||||
|
||||
async findOneByUserId(userId: string) {
|
||||
return db.query.credentialsTable.findFirst({
|
||||
where: eq(credentialsTable.user_id, userId)
|
||||
});
|
||||
}
|
||||
|
||||
async findPasswordCredentialsByUserId(userId: string) {
|
||||
return db.query.credentialsTable.findFirst({
|
||||
where: and(
|
||||
eq(credentialsTable.user_id, userId),
|
||||
eq(credentialsTable.type, CredentialsType.PASSWORD)
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
async findTOTPCredentialsByUserId(userId: string) {
|
||||
return db.query.credentialsTable.findFirst({
|
||||
where: and(
|
||||
eq(credentialsTable.user_id, userId),
|
||||
eq(credentialsTable.type, CredentialsType.TOTP)
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
async findOneById(id: string) {
|
||||
return db.query.credentialsTable.findFirst({
|
||||
where: eq(credentialsTable.id, id)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ 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 { CredentialsRepository } from '../repositories/credentials.repository';
|
||||
import type { HonoRequest } from 'hono';
|
||||
|
||||
@injectable()
|
||||
export class LoginRequestsService {
|
||||
|
|
@ -17,13 +18,9 @@ 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 loginRequestsRepository: LoginRequestsRepository,
|
||||
@inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository,
|
||||
) { }
|
||||
|
||||
async validate(data: SignInEmailDto) {
|
||||
|
||||
}
|
||||
|
||||
async create(data: RegisterEmailDto) {
|
||||
// generate a token, expiry date, and hash
|
||||
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm');
|
||||
|
|
@ -36,16 +33,36 @@ export class LoginRequestsService {
|
|||
});
|
||||
}
|
||||
|
||||
async verify(data: SignInEmailDto) {
|
||||
let existingUser = await this.usersRepository.findOneByUsername(data.username);
|
||||
async verify(data: SignInEmailDto, req: HonoRequest) {
|
||||
const requestIpAddress = req.header('x-real-ip');
|
||||
const requestIpCountry = req.header('x-vercel-ip-country');
|
||||
const existingUser = await this.usersRepository.findOneByUsername(data.username);
|
||||
|
||||
if (!existingUser) {
|
||||
throw BadRequest('User not found');
|
||||
}
|
||||
|
||||
const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id);
|
||||
|
||||
if (!credential) {
|
||||
throw BadRequest('Invalid credentials');
|
||||
}
|
||||
|
||||
return this.lucia.createSession(existingUser.id, {});
|
||||
if (!await this.tokensService.verifyHashedToken(credential.hashedPassword, data.password)) {
|
||||
throw BadRequest('Invalid credentials');
|
||||
}
|
||||
|
||||
const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id);
|
||||
|
||||
return this.lucia.createSession(existingUser.id, {
|
||||
ip_country: requestIpCountry || 'unknown',
|
||||
ip_address: requestIpAddress || 'unknown',
|
||||
twoFactorAuthEnabled:
|
||||
!!totpCredentials &&
|
||||
totpCredentials?.secret !== null &&
|
||||
totpCredentials?.secret !== '',
|
||||
isTwoFactorAuthenticated: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a new user and send a welcome email - or other onboarding process
|
||||
|
|
|
|||
17
src/lib/server/api/services/users.service.ts
Normal file
17
src/lib/server/api/services/users.service.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, injectable } from 'tsyringe';
|
||||
import type { UsersRepository } from '../repositories/users.repository';
|
||||
|
||||
@injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@inject('UsersRepository') private readonly usersRepository: UsersRepository
|
||||
) { }
|
||||
|
||||
async findOneByUsername(username: string) {
|
||||
return this.usersRepository.findOneByUsername(username);
|
||||
}
|
||||
|
||||
async findOneById(id: string) {
|
||||
return this.usersRepository.findOneById(id);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue