diff --git a/apps/backend/src/domains/users/auth/auth.constants.ts b/apps/backend/src/domains/users/auth/auth.constants.ts new file mode 100644 index 00000000..1caab7b5 --- /dev/null +++ b/apps/backend/src/domains/users/auth/auth.constants.ts @@ -0,0 +1,21 @@ +export const AUTH_COOKIE = { + ACCESS: "twizrr_access_token", + REFRESH: "twizrr_refresh_token", +} as const; + +export const AUTH_TTL = { + ACCESS_SECONDS: 15 * 60, + REFRESH_SECONDS: 7 * 24 * 60 * 60, + ACCESS_COOKIE_MS: 15 * 60 * 1000, + REFRESH_COOKIE_MS: 7 * 24 * 60 * 60 * 1000, +} as const; + +export const AUTH_STRATEGY = { + ACCESS: "jwt-access", + REFRESH: "jwt-refresh", +} as const; + +export const REDIRECT_TARGET = { + STORE_DASHBOARD: "/store/dashboard", + EXPLORE: "/explore", +} as const; diff --git a/apps/backend/src/domains/users/auth/auth.controller.ts b/apps/backend/src/domains/users/auth/auth.controller.ts index a6b0f94a..0eb94e09 100644 --- a/apps/backend/src/domains/users/auth/auth.controller.ts +++ b/apps/backend/src/domains/users/auth/auth.controller.ts @@ -1,7 +1,22 @@ -import { Body, Controller, HttpCode, HttpStatus, Post } from "@nestjs/common"; +import { + Body, + Controller, + HttpCode, + HttpStatus, + Post, + Req, + Res, + UseGuards, +} from "@nestjs/common"; import { Throttle } from "@nestjs/throttler"; +import type { Request, Response } from "express"; +import { JwtRefreshGuard } from "../../../common/guards/jwt-refresh.guard"; +import { AUTH_COOKIE, AUTH_TTL } from "./auth.constants"; import { AuthService } from "./auth.service"; +import { RefreshRequestUser, SessionMetadata } from "./auth.types"; +import { LoginDto } from "./dto/login.dto"; +import { ForgotPasswordDto, ResetPasswordDto } from "./dto/password-reset.dto"; import { RegisterEmailDto } from "./dto/register.dto"; import { SendOtpDto, @@ -13,18 +28,148 @@ import { export class AuthController { constructor(private readonly authService: AuthService) {} + private successEnvelope(data: T): { success: true; data: T } { + return { success: true, data }; + } + + private setAuthCookies( + response: Response, + tokens: { accessToken: string; refreshToken: string }, + ): void { + const isLocal = process.env.NODE_ENV === "development"; + const secure = !isLocal; + const sameSite = isLocal ? "lax" : "none"; + + response.cookie(AUTH_COOKIE.ACCESS, tokens.accessToken, { + httpOnly: true, + secure, + sameSite, + maxAge: AUTH_TTL.ACCESS_COOKIE_MS, + path: "/", + }); + response.cookie(AUTH_COOKIE.REFRESH, tokens.refreshToken, { + httpOnly: true, + secure, + sameSite, + maxAge: AUTH_TTL.REFRESH_COOKIE_MS, + path: "/", + }); + } + + private clearAuthCookies(response: Response): void { + const isLocal = process.env.NODE_ENV === "development"; + const secure = !isLocal; + const sameSite = isLocal ? "lax" : "none"; + + response.clearCookie(AUTH_COOKIE.ACCESS, { + httpOnly: true, + secure, + sameSite, + path: "/", + }); + response.clearCookie(AUTH_COOKIE.REFRESH, { + httpOnly: true, + secure, + sameSite, + path: "/", + }); + } + + private getSessionMetadata(request: Request): SessionMetadata { + return { + userAgent: request.headers["user-agent"], + ipAddress: request.ip, + }; + } + + private getRefreshCookie(request: Request): string | undefined { + const cookies = ( + request as Request & { + cookies?: Record; + } + ).cookies; + + return cookies?.[AUTH_COOKIE.REFRESH]; + } + @Throttle({ default: { limit: 5, ttl: 3_600_000 } }) @Post("register/email") async registerEmail(@Body() dto: RegisterEmailDto) { return this.authService.registerEmail(dto); } + @Throttle({ default: { limit: 10, ttl: 900_000 } }) + @Post("login") + @HttpCode(HttpStatus.OK) + async login( + @Body() dto: LoginDto, + @Req() request: Request, + @Res({ passthrough: true }) response: Response, + ) { + const result = await this.authService.login( + dto, + this.getSessionMetadata(request), + ); + + this.setAuthCookies(response, result); + + return this.successEnvelope({ redirectTo: result.redirectTo }); + } + + @Post("logout") + @HttpCode(HttpStatus.OK) + async logout( + @Req() request: Request, + @Res({ passthrough: true }) response: Response, + ) { + const refreshToken = this.getRefreshCookie(request); + const result = await this.authService.logout(refreshToken); + this.clearAuthCookies(response); + + return this.successEnvelope(result); + } + + @UseGuards(JwtRefreshGuard) + @Post("refresh") + @HttpCode(HttpStatus.OK) + async refresh( + @Req() request: Request, + @Res({ passthrough: true }) response: Response, + ) { + const refreshUser = (request as Request & { user: RefreshRequestUser }) + .user; + const tokens = await this.authService.refresh( + refreshUser, + this.getSessionMetadata(request), + ); + + this.setAuthCookies(response, tokens); + + return this.successEnvelope({ refreshed: true }); + } + @Post("email/verify") @HttpCode(HttpStatus.OK) async verifyEmail(@Body() dto: VerifyEmailDto) { return this.authService.verifyEmail(dto); } + @Throttle({ default: { limit: 3, ttl: 3_600_000 } }) + @Post("forgot-password") + @HttpCode(HttpStatus.OK) + async forgotPassword(@Body() dto: ForgotPasswordDto) { + const result = await this.authService.forgotPassword(dto); + return this.successEnvelope(result); + } + + @Throttle({ default: { limit: 3, ttl: 3_600_000 } }) + @Post("reset-password") + @HttpCode(HttpStatus.OK) + async resetPassword(@Body() dto: ResetPasswordDto) { + const result = await this.authService.resetPassword(dto); + return this.successEnvelope(result); + } + @Throttle({ default: { limit: 3, ttl: 3_600_000 } }) @Post("otp/send") @HttpCode(HttpStatus.OK) diff --git a/apps/backend/src/domains/users/auth/auth.module.ts b/apps/backend/src/domains/users/auth/auth.module.ts index 46249970..f2fca1f7 100644 --- a/apps/backend/src/domains/users/auth/auth.module.ts +++ b/apps/backend/src/domains/users/auth/auth.module.ts @@ -1,15 +1,46 @@ import { Module } from "@nestjs/common"; +import { ConfigModule, ConfigService } from "@nestjs/config"; +import { + JwtModule, + type JwtModuleOptions, + type JwtSignOptions, +} from "@nestjs/jwt"; +import { PassportModule } from "@nestjs/passport"; import { AfricasTalkingModule } from "../../../integrations/africastalking/africastalking.module"; import { ResendModule } from "../../../integrations/resend/resend.module"; import { AuthController } from "./auth.controller"; import { AuthService } from "./auth.service"; +import { JwtAccessStrategy } from "./strategies/jwt.strategy"; +import { JwtRefreshStrategy } from "./strategies/jwt-refresh.strategy"; import { LocalStrategy } from "./strategies/local.strategy"; +type JwtExpiresIn = NonNullable; + +const jwtTtl = (configService: ConfigService, key: string): JwtExpiresIn => + configService.getOrThrow(key) as JwtExpiresIn; + @Module({ - imports: [ResendModule, AfricasTalkingModule], + imports: [ + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService): JwtModuleOptions => ({ + secret: configService.getOrThrow("jwt.accessSecret"), + signOptions: { expiresIn: jwtTtl(configService, "jwt.accessTtl") }, + }), + inject: [ConfigService], + }), + ResendModule, + AfricasTalkingModule, + ], controllers: [AuthController], - providers: [AuthService, LocalStrategy], + providers: [ + AuthService, + LocalStrategy, + JwtAccessStrategy, + JwtRefreshStrategy, + ], exports: [AuthService], }) export class UsersAuthModule {} diff --git a/apps/backend/src/domains/users/auth/auth.service.ts b/apps/backend/src/domains/users/auth/auth.service.ts index 43afceed..e87ecc0d 100644 --- a/apps/backend/src/domains/users/auth/auth.service.ts +++ b/apps/backend/src/domains/users/auth/auth.service.ts @@ -3,15 +3,34 @@ import { ConflictException, Injectable, NotFoundException, + UnauthorizedException, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; +import { JwtService } from "@nestjs/jwt"; import { OTPPurpose, User, UserRole } from "@prisma/client"; import * as bcrypt from "bcrypt"; -import { createHmac, randomInt, timingSafeEqual } from "crypto"; +import { + createHash, + createHmac, + randomBytes, + randomInt, + randomUUID, + timingSafeEqual, +} from "crypto"; import { PrismaService } from "../../../core/prisma/prisma.service"; import { AfricasTalkingClient } from "../../../integrations/africastalking/africastalking.client"; import { ResendClient } from "../../../integrations/resend/resend.client"; +import { AUTH_TTL, REDIRECT_TARGET } from "./auth.constants"; +import { + AccessTokenPayload, + AuthCookieTokens, + RefreshRequestUser, + RefreshTokenPayload, + SessionMetadata, +} from "./auth.types"; +import { LoginDto } from "./dto/login.dto"; +import { ForgotPasswordDto, ResetPasswordDto } from "./dto/password-reset.dto"; import { RegisterEmailDto } from "./dto/register.dto"; import { SendOtpDto, @@ -24,6 +43,9 @@ const OTP_TTL_MINUTES = 15; const OTP_TTL_MS = OTP_TTL_MINUTES * 60 * 1000; const MINIMUM_REGISTRATION_AGE = 13; const MAX_OTP_ATTEMPTS = 5; +const PASSWORD_RESET_TTL_MS = 15 * 60 * 1000; +const GENERIC_PASSWORD_RESET_MESSAGE = + "If an account exists for that email, a password reset link has been sent."; export interface AuthUserResponse { id: string; @@ -42,9 +64,13 @@ export interface AuthUserResponse { @Injectable() export class AuthService { private readonly otpSecret: string; + private readonly accessTokenSecret: string; + private readonly refreshTokenSecret: string; + private readonly appWebUrl: string; constructor( private readonly prisma: PrismaService, + private readonly jwtService: JwtService, private readonly resendClient: ResendClient, private readonly africasTalkingClient: AfricasTalkingClient, configService: ConfigService, @@ -59,6 +85,14 @@ export class AuthService { } this.otpSecret = configuredSecret; + this.accessTokenSecret = + configService.getOrThrow("jwt.accessSecret"); + this.refreshTokenSecret = + configService.getOrThrow("jwt.refreshSecret"); + this.appWebUrl = + configService.get("app.webUrl") || + configService.get("WEB_URL") || + "http://localhost:3000"; } async registerEmail( @@ -226,6 +260,166 @@ export class AuthService { return this.sendPhoneOtp(dto); } + async login( + dto: LoginDto, + metadata: SessionMetadata, + ): Promise { + const user = await this.prisma.user.findFirst({ + where: { + email: dto.email, + isActive: true, + deletedAt: null, + }, + include: { + storeProfile: { + select: { id: true }, + }, + }, + }); + + if (!user || !(await bcrypt.compare(dto.password, user.passwordHash))) { + throw new UnauthorizedException({ + message: "Invalid email or password", + code: "AUTH_INVALID_CREDENTIALS", + }); + } + + const tokens = await this.createSessionTokens({ + user: { + id: user.id, + email: user.email, + role: user.role, + storeId: user.storeProfile?.id, + }, + metadata, + }); + + return { + ...tokens, + redirectTo: user.storeProfile + ? REDIRECT_TARGET.STORE_DASHBOARD + : REDIRECT_TARGET.EXPLORE, + }; + } + + async refresh( + refreshUser: RefreshRequestUser, + metadata: SessionMetadata, + ): Promise { + const tokens = await this.signTokenPair({ + sub: refreshUser.user.id, + email: refreshUser.user.email, + role: refreshUser.user.role, + storeId: refreshUser.user.storeId, + }); + + await this.prisma.session.update({ + where: { id: refreshUser.sessionId }, + data: { + refreshTokenHash: this.hashToken(tokens.refreshToken), + expiresAt: new Date(Date.now() + AUTH_TTL.REFRESH_COOKIE_MS), + userAgent: metadata.userAgent, + ipAddress: metadata.ipAddress, + }, + }); + + return tokens; + } + + async logout(refreshToken: string | undefined): Promise<{ loggedOut: true }> { + if (!refreshToken) { + return { loggedOut: true }; + } + + await this.prisma.session.updateMany({ + where: { + refreshTokenHash: this.hashToken(refreshToken), + revokedAt: null, + }, + data: { revokedAt: new Date() }, + }); + + return { loggedOut: true }; + } + + async forgotPassword(dto: ForgotPasswordDto): Promise<{ message: string }> { + const user = await this.prisma.user.findFirst({ + where: { + email: dto.email, + isActive: true, + deletedAt: null, + }, + select: { id: true, email: true }, + }); + + if (!user) { + return { message: GENERIC_PASSWORD_RESET_MESSAGE }; + } + + const resetToken = randomBytes(32).toString("hex"); + const resetUrl = `${this.appWebUrl}/reset-password?token=${resetToken}`; + + await this.prisma.user.update({ + where: { id: user.id }, + data: { + resetToken: this.hashToken(resetToken), + resetTokenExpiry: new Date(Date.now() + PASSWORD_RESET_TTL_MS), + }, + }); + + await this.resendClient.sendEmail( + user.email, + "Reset your password", + `

Use this secure link to reset your twizrr password:

Reset password

This link expires in 15 minutes.

`, + ); + + return { message: GENERIC_PASSWORD_RESET_MESSAGE }; + } + + async resetPassword(dto: ResetPasswordDto): Promise<{ reset: true }> { + const user = await this.prisma.user.findFirst({ + where: { + resetToken: this.hashToken(dto.token), + resetTokenExpiry: { gt: new Date() }, + isActive: true, + deletedAt: null, + }, + select: { id: true }, + }); + + if (!user) { + throw new BadRequestException({ + message: "Invalid or expired password reset token", + code: "PASSWORD_RESET_INVALID", + }); + } + + const passwordHash = await bcrypt.hash( + dto.newPassword, + PASSWORD_SALT_ROUNDS, + ); + + await this.prisma.$transaction([ + this.prisma.user.update({ + where: { id: user.id }, + data: { + passwordHash, + resetToken: null, + resetTokenExpiry: null, + }, + }), + this.prisma.session.updateMany({ + where: { + userId: user.id, + revokedAt: null, + }, + data: { revokedAt: new Date() }, + }), + ]); + + return { reset: true }; + } + private async assertUniqueRegistrationFields( email: string, phone: string, @@ -276,6 +470,68 @@ export class AuthService { } } + private async createSessionTokens(params: { + user: { + id: string; + email: string; + role: UserRole; + storeId?: string; + }; + metadata: SessionMetadata; + }): Promise { + const tokens = await this.signTokenPair({ + sub: params.user.id, + email: params.user.email, + role: params.user.role, + storeId: params.user.storeId, + }); + + await this.prisma.session.create({ + data: { + userId: params.user.id, + refreshTokenHash: this.hashToken(tokens.refreshToken), + userAgent: params.metadata.userAgent, + ipAddress: params.metadata.ipAddress, + expiresAt: new Date(Date.now() + AUTH_TTL.REFRESH_COOKIE_MS), + }, + }); + + return tokens; + } + + private async signTokenPair(params: { + sub: string; + email: string; + role: UserRole; + storeId?: string; + }): Promise { + const accessPayload: AccessTokenPayload = { + ...params, + jti: randomUUID(), + }; + const refreshPayload: RefreshTokenPayload = { + ...params, + jti: randomUUID(), + }; + + const [accessToken, refreshToken] = await Promise.all([ + this.jwtService.signAsync(accessPayload, { + secret: this.accessTokenSecret, + expiresIn: AUTH_TTL.ACCESS_SECONDS, + }), + this.jwtService.signAsync(refreshPayload, { + secret: this.refreshTokenSecret, + expiresIn: AUTH_TTL.REFRESH_SECONDS, + }), + ]); + + return { accessToken, refreshToken }; + } + + private hashToken(token: string): string { + return createHash("sha256").update(token).digest("hex"); + } + private assertAgeAllowed(dateOfBirth: Date): void { if (Number.isNaN(dateOfBirth.getTime()) || dateOfBirth > new Date()) { throw new BadRequestException({ diff --git a/apps/backend/src/domains/users/auth/auth.types.ts b/apps/backend/src/domains/users/auth/auth.types.ts new file mode 100644 index 00000000..0acf5499 --- /dev/null +++ b/apps/backend/src/domains/users/auth/auth.types.ts @@ -0,0 +1,44 @@ +import { UserRole } from "@prisma/client"; +import type { Request } from "express"; + +export interface AccessTokenPayload { + sub: string; + email: string; + role: UserRole; + storeId?: string; + jti: string; +} + +export interface RefreshTokenPayload extends AccessTokenPayload {} + +export interface AuthenticatedRequestUser { + id: string; + sub: string; + email: string; + role: UserRole; + storeId?: string; + username: string | null; + displayName: string | null; + isEmailVerified: boolean; + isPhoneVerified: boolean; +} + +export interface RefreshRequestUser { + sessionId: string; + refreshToken: string; + user: AuthenticatedRequestUser; +} + +export interface SessionMetadata { + userAgent?: string; + ipAddress?: string; +} + +export interface AuthCookieTokens { + accessToken: string; + refreshToken: string; +} + +export type RequestWithCookies = Request & { + cookies?: Record; +}; diff --git a/apps/backend/src/domains/users/auth/dto/login.dto.ts b/apps/backend/src/domains/users/auth/dto/login.dto.ts new file mode 100644 index 00000000..27c4bb0e --- /dev/null +++ b/apps/backend/src/domains/users/auth/dto/login.dto.ts @@ -0,0 +1,14 @@ +import { Transform } from "class-transformer"; +import { IsEmail, IsString, MinLength } from "class-validator"; + +export class LoginDto { + @Transform(({ value }) => + typeof value === "string" ? value.trim().toLowerCase() : value, + ) + @IsEmail() + email!: string; + + @IsString() + @MinLength(1) + password!: string; +} diff --git a/apps/backend/src/domains/users/auth/dto/password-reset.dto.ts b/apps/backend/src/domains/users/auth/dto/password-reset.dto.ts new file mode 100644 index 00000000..13d6ca25 --- /dev/null +++ b/apps/backend/src/domains/users/auth/dto/password-reset.dto.ts @@ -0,0 +1,32 @@ +import { Transform } from "class-transformer"; +import { + IsEmail, + IsString, + Matches, + MaxLength, + MinLength, +} from "class-validator"; + +export class ForgotPasswordDto { + @Transform(({ value }) => + typeof value === "string" ? value.trim().toLowerCase() : value, + ) + @IsEmail() + email!: string; +} + +export class ResetPasswordDto { + @IsString() + @MinLength(32) + @MaxLength(256) + token!: string; + + @IsString() + @MinLength(8) + @MaxLength(128) + @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z\d]).+$/, { + message: + "newPassword must contain uppercase, lowercase, number, and special character", + }) + newPassword!: string; +} diff --git a/apps/backend/src/domains/users/auth/strategies/jwt-refresh.strategy.ts b/apps/backend/src/domains/users/auth/strategies/jwt-refresh.strategy.ts new file mode 100644 index 00000000..c90d671c --- /dev/null +++ b/apps/backend/src/domains/users/auth/strategies/jwt-refresh.strategy.ts @@ -0,0 +1,109 @@ +import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { PassportStrategy } from "@nestjs/passport"; +import { ExtractJwt, Strategy } from "passport-jwt"; +import { createHash } from "crypto"; + +import { PrismaService } from "../../../../core/prisma/prisma.service"; +import { AUTH_COOKIE, AUTH_STRATEGY } from "../auth.constants"; +import { + AuthenticatedRequestUser, + RefreshRequestUser, + RefreshTokenPayload, + RequestWithCookies, +} from "../auth.types"; + +const refreshTokenExtractor = (request: RequestWithCookies): string | null => { + return request.cookies?.[AUTH_COOKIE.REFRESH] || null; +}; + +@Injectable() +export class JwtRefreshStrategy extends PassportStrategy( + Strategy, + AUTH_STRATEGY.REFRESH, +) { + constructor( + configService: ConfigService, + private readonly prisma: PrismaService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([refreshTokenExtractor]), + ignoreExpiration: false, + passReqToCallback: true, + secretOrKey: configService.getOrThrow("jwt.refreshSecret"), + }); + } + + async validate( + request: RequestWithCookies, + payload: RefreshTokenPayload, + ): Promise { + const refreshToken = refreshTokenExtractor(request); + + if (!refreshToken) { + throw new UnauthorizedException({ + message: "Refresh token required", + code: "AUTH_REFRESH_REQUIRED", + }); + } + + const session = await this.prisma.session.findFirst({ + where: { + userId: payload.sub, + refreshTokenHash: this.hashToken(refreshToken), + revokedAt: null, + expiresAt: { gt: new Date() }, + user: { + isActive: true, + deletedAt: null, + }, + }, + include: { + user: { + select: { + id: true, + email: true, + role: true, + username: true, + displayName: true, + emailVerified: true, + phoneVerified: true, + storeProfile: { + select: { id: true }, + }, + }, + }, + }, + }); + + if (!session) { + throw new UnauthorizedException({ + message: "Invalid refresh token", + code: "AUTH_REFRESH_INVALID", + }); + } + + const storeId = session.user.storeProfile?.id; + const user: AuthenticatedRequestUser = { + id: session.user.id, + sub: session.user.id, + email: session.user.email, + role: session.user.role, + storeId, + username: session.user.username, + displayName: session.user.displayName, + isEmailVerified: session.user.emailVerified, + isPhoneVerified: session.user.phoneVerified, + }; + + return { + sessionId: session.id, + refreshToken, + user, + }; + } + + private hashToken(token: string): string { + return createHash("sha256").update(token).digest("hex"); + } +} diff --git a/apps/backend/src/domains/users/auth/strategies/jwt.strategy.ts b/apps/backend/src/domains/users/auth/strategies/jwt.strategy.ts new file mode 100644 index 00000000..d802e451 --- /dev/null +++ b/apps/backend/src/domains/users/auth/strategies/jwt.strategy.ts @@ -0,0 +1,78 @@ +import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { PassportStrategy } from "@nestjs/passport"; +import { ExtractJwt, Strategy } from "passport-jwt"; + +import { PrismaService } from "../../../../core/prisma/prisma.service"; +import { AUTH_COOKIE, AUTH_STRATEGY } from "../auth.constants"; +import { + AccessTokenPayload, + AuthenticatedRequestUser, + RequestWithCookies, +} from "../auth.types"; + +const accessTokenExtractor = (request: RequestWithCookies): string | null => { + return request.cookies?.[AUTH_COOKIE.ACCESS] || null; +}; + +@Injectable() +export class JwtAccessStrategy extends PassportStrategy( + Strategy, + AUTH_STRATEGY.ACCESS, +) { + constructor( + configService: ConfigService, + private readonly prisma: PrismaService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([accessTokenExtractor]), + ignoreExpiration: false, + secretOrKey: configService.getOrThrow("jwt.accessSecret"), + }); + } + + async validate( + payload: AccessTokenPayload, + ): Promise { + const user = await this.prisma.user.findFirst({ + where: { + id: payload.sub, + isActive: true, + deletedAt: null, + }, + select: { + id: true, + email: true, + role: true, + username: true, + displayName: true, + emailVerified: true, + phoneVerified: true, + storeProfile: { + select: { id: true }, + }, + }, + }); + + if (!user) { + throw new UnauthorizedException({ + message: "Invalid token", + code: "AUTH_INVALID_TOKEN", + }); + } + + const storeId = user.storeProfile?.id; + + return { + id: user.id, + sub: user.id, + email: user.email, + role: user.role, + storeId, + username: user.username, + displayName: user.displayName, + isEmailVerified: user.emailVerified, + isPhoneVerified: user.phoneVerified, + }; + } +} diff --git a/apps/backend/src/domains/users/auth/strategies/local.strategy.ts b/apps/backend/src/domains/users/auth/strategies/local.strategy.ts index 8034dbb4..9068ad2c 100644 --- a/apps/backend/src/domains/users/auth/strategies/local.strategy.ts +++ b/apps/backend/src/domains/users/auth/strategies/local.strategy.ts @@ -4,8 +4,8 @@ import { Injectable, UnauthorizedException } from "@nestjs/common"; export class LocalStrategy { validate(): never { throw new UnauthorizedException({ - message: "Password login is not available in this task", - code: "LOGIN_NOT_IMPLEMENTED", + message: "Use POST /auth/login for password authentication", + code: "AUTH_ROUTE_REQUIRED", }); } } diff --git a/apps/backend/src/modules/auth/auth.module.ts b/apps/backend/src/modules/auth/auth.module.ts index 4f45008a..76d88c23 100644 --- a/apps/backend/src/modules/auth/auth.module.ts +++ b/apps/backend/src/modules/auth/auth.module.ts @@ -6,11 +6,8 @@ import { type JwtSignOptions, } from "@nestjs/jwt"; import { ConfigModule, ConfigService } from "@nestjs/config"; -import { AuthController } from "./auth.controller"; import { InternalAuthController } from "./internal-auth.controller"; import { AuthService } from "./auth.service"; -import { JwtAccessStrategy } from "./strategies/jwt-access.strategy"; -import { JwtRefreshStrategy } from "./strategies/jwt-refresh.strategy"; import { PrismaModule } from "../../prisma/prisma.module"; import { RedisModule } from "../../redis/redis.module"; import { AdminModule } from "../admin/admin.module"; @@ -42,8 +39,8 @@ const jwtTtl = (configService: ConfigService, key: string): JwtExpiresIn => forwardRef(() => AdminModule), forwardRef(() => WhatsAppModule), ], - controllers: [AuthController, InternalAuthController], - providers: [AuthService, JwtAccessStrategy, JwtRefreshStrategy], + controllers: [InternalAuthController], + providers: [AuthService], exports: [AuthService], }) export class AuthModule {}