Update package dependencies and refactor authentication module

- Added sharp dependency for image processing in package.json.
- Updated argon2 dependency version to 0.44.0 for enhanced security.
- Removed unused @nestjs/jwt dependency and refactored authentication module to utilize JoseJwtService for JWT handling.
- Adjusted type definitions for @types/node and @types/pg to ensure compatibility across applications.
- Cleaned up package.json files in BFF and Portal applications for consistency and improved dependency management.
This commit is contained in:
barsa 2025-12-11 12:03:31 +09:00
parent fee93cc02b
commit 424f257bd7
14 changed files with 529 additions and 294 deletions

View File

@ -36,13 +36,12 @@
"@nestjs/common": "^11.1.9",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.9",
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.9",
"@prisma/adapter-pg": "^7.1.0",
"@prisma/client": "^7.1.0",
"@sendgrid/mail": "^8.1.6",
"argon2": "^0.43.0",
"argon2": "^0.44.0",
"bullmq": "^5.65.1",
"cookie-parser": "^1.4.7",
"helmet": "^8.1.0",
@ -70,10 +69,9 @@
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/node": "24.10.3",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/pg": "^8.15.6",
"@types/pg": "^8.16.0",
"@types/ssh2-sftp-client": "^9.0.6",
"@types/supertest": "^6.0.3",
"jest": "^30.2.0",

View File

@ -1,5 +1,4 @@
import { Injectable, UnauthorizedException, BadRequestException, Inject } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";
import * as argon2 from "argon2";
import { UsersFacade } from "@bff/modules/users/application/users.facade.js";
@ -38,7 +37,6 @@ export class AuthFacade {
constructor(
private readonly usersFacade: UsersFacade,
private readonly mappingsService: MappingsService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly whmcsService: WhmcsService,
private readonly salesforceService: SalesforceService,

View File

@ -1,7 +1,5 @@
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { ConfigService } from "@nestjs/config";
import { APP_GUARD } from "@nestjs/core";
import { AuthFacade } from "./application/auth.facade.js";
import { AuthController } from "./presentation/http/auth.controller.js";
@ -15,6 +13,7 @@ import { TokenBlacklistService } from "./infra/token/token-blacklist.service.js"
import { EmailModule } from "@bff/infra/email/email.module.js";
import { CacheModule } from "@bff/infra/cache/cache.module.js";
import { AuthTokenService } from "./infra/token/token.service.js";
import { JoseJwtService } from "./infra/token/jose-jwt.service.js";
import { SignupWorkflowService } from "./infra/workflows/workflows/signup-workflow.service.js";
import { PasswordWorkflowService } from "./infra/workflows/workflows/password-workflow.service.js";
import { WhmcsLinkWorkflowService } from "./infra/workflows/workflows/whmcs-link-workflow.service.js";
@ -25,13 +24,6 @@ import { AuthRateLimitService } from "./infra/rate-limiting/auth-rate-limit.serv
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
useFactory: (configService: ConfigService) => ({
secret: configService.get("JWT_SECRET"),
signOptions: { expiresIn: configService.get("JWT_EXPIRES_IN", "7d") },
}),
inject: [ConfigService],
}),
UsersModule,
MappingsModule,
IntegrationsModule,
@ -45,6 +37,7 @@ import { AuthRateLimitService } from "./infra/rate-limiting/auth-rate-limit.serv
LocalStrategy,
TokenBlacklistService,
AuthTokenService,
JoseJwtService,
SignupWorkflowService,
PasswordWorkflowService,
WhmcsLinkWorkflowService,

View File

@ -0,0 +1,53 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { SignJWT, decodeJwt, jwtVerify, errors, type JWTPayload } from "jose";
import { parseJwtExpiry } from "../../utils/jwt-expiry.util.js";
@Injectable()
export class JoseJwtService {
private readonly secretKey: Uint8Array;
constructor(private readonly configService: ConfigService) {
const secret = configService.get<string>("JWT_SECRET");
if (!secret) {
throw new Error("JWT_SECRET is required in environment variables");
}
this.secretKey = new TextEncoder().encode(secret);
}
async sign(payload: JWTPayload, expiresIn: string): Promise<string> {
const expiresInSeconds = parseJwtExpiry(expiresIn);
const nowSeconds = Math.floor(Date.now() / 1000);
return new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt(nowSeconds)
.setExpirationTime(nowSeconds + expiresInSeconds)
.sign(this.secretKey);
}
async verify<T extends JWTPayload>(token: string): Promise<T> {
const { payload } = await jwtVerify(token, this.secretKey);
return payload as T;
}
async verifyAllowExpired<T extends JWTPayload>(token: string): Promise<T | null> {
try {
return await this.verify<T>(token);
} catch (err) {
if (err instanceof errors.JWTExpired) {
return this.decode<T>(token);
}
throw err;
}
}
decode<T extends JWTPayload>(token: string): T | null {
try {
return decodeJwt(token) as T;
} catch {
return null;
}
}
}

View File

@ -1,17 +1,17 @@
import { Injectable, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { createHash } from "crypto";
import { Redis } from "ioredis";
import { Logger } from "nestjs-pino";
import { parseJwtExpiry } from "../../utils/jwt-expiry.util.js";
import { JoseJwtService } from "./jose-jwt.service.js";
@Injectable()
export class TokenBlacklistService {
constructor(
@Inject("REDIS_CLIENT") private readonly redis: Redis,
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private readonly jwtService: JoseJwtService,
@Inject(Logger) private readonly logger: Logger
) {}
@ -24,14 +24,14 @@ export class TokenBlacklistService {
// Use JwtService to safely decode and validate token
try {
const decoded: unknown = this.jwtService.decode(token);
const decoded = this.jwtService.decode<{ sub?: string; exp?: number }>(token);
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
this.logger.warn("Invalid JWT payload structure for blacklisting");
return;
}
const { sub, exp } = decoded as { sub?: unknown; exp?: unknown };
const { sub, exp } = decoded;
if (typeof sub !== "string" || typeof exp !== "number") {
this.logger.warn("Invalid JWT payload structure for blacklisting");
return;

View File

@ -4,17 +4,18 @@ import {
UnauthorizedException,
ServiceUnavailableException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";
import { Redis } from "ioredis";
import { Logger } from "nestjs-pino";
import { randomBytes, createHash } from "crypto";
import type { JWTPayload } from "jose";
import type { AuthTokens } from "@customer-portal/domain/auth";
import type { User } from "@customer-portal/domain/customer";
import { UsersFacade } from "@bff/modules/users/application/users.facade.js";
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
import { JoseJwtService } from "./jose-jwt.service.js";
export interface RefreshTokenPayload {
export interface RefreshTokenPayload extends JWTPayload {
userId: string;
tokenId: string;
deviceId?: string;
@ -49,7 +50,7 @@ export class AuthTokenService {
private readonly maintenanceMessage: string;
constructor(
private readonly jwtService: JwtService,
private readonly jwtService: JoseJwtService,
private readonly configService: ConfigService,
@Inject("REDIS_CLIENT") private readonly redis: Redis,
@Inject(Logger) private readonly logger: Logger,
@ -124,13 +125,9 @@ export class AuthTokenService {
};
// Generate tokens
const accessToken = this.jwtService.sign(accessPayload, {
expiresIn: this.ACCESS_TOKEN_EXPIRY,
});
const accessToken = await this.jwtService.sign(accessPayload, this.ACCESS_TOKEN_EXPIRY);
const refreshToken = this.jwtService.sign(refreshPayload, {
expiresIn: this.REFRESH_TOKEN_EXPIRY,
});
const refreshToken = await this.jwtService.sign(refreshPayload, this.REFRESH_TOKEN_EXPIRY);
// Store refresh token family in Redis
const refreshTokenHash = this.hashToken(refreshToken);
@ -211,7 +208,7 @@ export class AuthTokenService {
}
try {
// Verify refresh token
const payload = this.jwtService.verify<RefreshTokenPayload>(refreshToken);
const payload = await this.jwtService.verify<RefreshTokenPayload>(refreshToken);
if (payload.type !== "refresh") {
this.logger.warn("Token presented to refresh endpoint is not a refresh token", {

View File

@ -6,7 +6,6 @@ import {
NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { Logger } from "nestjs-pino";
import * as argon2 from "argon2";
import type { Request } from "express";
@ -16,6 +15,7 @@ import { EmailService } from "@bff/infra/email/email.service.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { AuthTokenService } from "../../token/token.service.js";
import { AuthRateLimitService } from "../../rate-limiting/auth-rate-limit.service.js";
import { JoseJwtService } from "../../token/jose-jwt.service.js";
import {
type PasswordChangeResult,
type ChangePasswordRequest,
@ -30,7 +30,7 @@ export class PasswordWorkflowService {
private readonly auditService: AuditService,
private readonly configService: ConfigService,
private readonly emailService: EmailService,
private readonly jwtService: JwtService,
private readonly jwtService: JoseJwtService,
private readonly tokenService: AuthTokenService,
private readonly authRateLimitService: AuthRateLimitService,
@Inject(Logger) private readonly logger: Logger
@ -99,10 +99,7 @@ export class PasswordWorkflowService {
return;
}
const token = this.jwtService.sign(
{ sub: user.id, purpose: "password_reset" },
{ expiresIn: "15m" }
);
const token = await this.jwtService.sign({ sub: user.id, purpose: "password_reset" }, "15m");
const appBase = this.configService.get<string>("APP_BASE_URL", "http://localhost:3000");
const resetUrl = `${appBase}/auth/reset-password?token=${encodeURIComponent(token)}`;
@ -130,7 +127,7 @@ export class PasswordWorkflowService {
async resetPassword(token: string, newPassword: string): Promise<PasswordChangeResult> {
try {
const payload = this.jwtService.verify<{ sub: string; purpose: string }>(token);
const payload = await this.jwtService.verify<{ sub: string; purpose: string }>(token);
if (payload.purpose !== "password_reset") {
throw new BadRequestException("Invalid token");
}

View File

@ -12,7 +12,6 @@ import {
} from "@nestjs/common";
import type { Request, Response } from "express";
import { RateLimitGuard, RateLimit } from "@bff/core/rate-limiting/index.js";
import { JwtService } from "@nestjs/jwt";
import { AuthFacade } from "@bff/modules/auth/application/auth.facade.js";
import { LocalAuthGuard } from "./guards/local-auth.guard.js";
import {
@ -25,6 +24,7 @@ import { ZodValidationPipe } from "nestjs-zod";
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard.js";
import { SalesforceWriteThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-write-throttle.guard.js";
import { JoseJwtService } from "../../infra/token/jose-jwt.service.js";
// Import Zod schemas from domain
import {
@ -108,7 +108,7 @@ const calculateCookieMaxAge = (isoTimestamp: string): number => {
export class AuthController {
constructor(
private authFacade: AuthFacade,
private readonly jwtService: JwtService
private readonly jwtService: JoseJwtService
) {}
private setAuthCookies(res: Response, tokens: AuthTokens): void {
@ -205,16 +205,9 @@ export class AuthController {
let userId = req.user?.id;
if (!userId && token) {
try {
const payload = await this.jwtService.verifyAsync<{ sub?: string }>(token, {
ignoreExpiration: true,
});
if (payload?.sub) {
userId = payload.sub;
}
} catch {
// Ignore verification errors we still want to clear client cookies.
const payload = await this.jwtService.verifyAllowExpired<{ sub?: string }>(token);
if (payload?.sub) {
userId = payload.sub;
}
}

View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -28,7 +28,6 @@
"react": "19.2.1",
"react-dom": "19.2.1",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
"world-countries": "^5.1.0",
"zod": "4.1.13",
"zustand": "^5.0.9"
@ -37,7 +36,6 @@
"@next/bundle-analyzer": "^16.0.8",
"@tailwindcss/postcss": "^4.1.17",
"@tanstack/react-query-devtools": "^5.91.1",
"@types/node": "24.10.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.1.17",

View File

@ -3,7 +3,7 @@
@import "../styles/tokens.css";
@import "../styles/utilities.css";
@import "../styles/responsive.css";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));

View File

@ -58,6 +58,7 @@
"globals": "^16.5.0",
"husky": "^9.1.7",
"prettier": "^3.7.4",
"sharp": "^0.33.5",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.49.0"

View File

@ -136,7 +136,6 @@
"zod": "4.1.13"
},
"devDependencies": {
"@types/node": "24.10.3",
"typescript": "5.9.3"
}
}

690
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff