Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/backend/src/domains/users/auth/auth.constants.ts
Original file line number Diff line number Diff line change
@@ -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;
147 changes: 146 additions & 1 deletion apps/backend/src/domains/users/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,18 +28,148 @@ import {
export class AuthController {
constructor(private readonly authService: AuthService) {}

private successEnvelope<T>(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<string, string | undefined>;
}
).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 });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@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);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@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 });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@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);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Throttle({ default: { limit: 3, ttl: 3_600_000 } })
@Post("otp/send")
@HttpCode(HttpStatus.OK)
Expand Down
35 changes: 33 additions & 2 deletions apps/backend/src/domains/users/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -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<JwtSignOptions["expiresIn"]>;

const jwtTtl = (configService: ConfigService, key: string): JwtExpiresIn =>
configService.getOrThrow<string>(key) as JwtExpiresIn;

@Module({
imports: [ResendModule, AfricasTalkingModule],
imports: [
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService): JwtModuleOptions => ({
secret: configService.getOrThrow<string>("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 {}
Loading
Loading