From 2cf1e89e02dbc2ff1dc1bd0cd4183454851c0d45 Mon Sep 17 00:00:00 2001 From: AlirizaSari Date: Tue, 16 Dec 2025 11:04:18 +0100 Subject: [PATCH 1/2] feat: add session management with user profile retrieval and update session cookie settings --- package-lock.json | 111 ++++++++++++++++++ package.json | 3 + src/main.ts | 5 +- .../controllers/user.controller.ts | 20 +++- 4 files changed, 135 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index c271b57..0804ad1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,11 @@ "@node-rs/argon2": "^2.0.2", "@prisma/adapter-mariadb": "^7.1.0", "@prisma/client": "^7.1.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", "cookie-parser": "^1.4.7", "dotenv": "^17.2.3", + "express-session": "^1.18.2", "helmet": "^8.1.0", "prisma": "^7.1.0", "reflect-metadata": "^0.2.2", @@ -3376,6 +3379,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4889,6 +4898,23 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5932,6 +5958,46 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -7892,6 +7958,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.31", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.31.tgz", + "integrity": "sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==", + "license": "MIT" + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -8562,6 +8634,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9112,6 +9193,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -10513,6 +10603,18 @@ "node": ">=8" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uint8array-extras": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", @@ -10669,6 +10771,15 @@ } } }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 959f5ea..0099035 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,11 @@ "@node-rs/argon2": "^2.0.2", "@prisma/adapter-mariadb": "^7.1.0", "@prisma/client": "^7.1.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", "cookie-parser": "^1.4.7", "dotenv": "^17.2.3", + "express-session": "^1.18.2", "helmet": "^8.1.0", "prisma": "^7.1.0", "reflect-metadata": "^0.2.2", diff --git a/src/main.ts b/src/main.ts index 96dd0c3..b3398d5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -37,14 +37,15 @@ async function bootstrap() { app.use( session({ secret: process.env.SESSION_SECRET, - resave: false, + resave: true, saveUninitialized: false, + rolling: true, // Reset maxAge on every request cookie: { // For cross-site requests (frontend != backend) browsers require SameSite='None' and Secure=true sameSite: isProduction ? 'none' : 'lax', secure: isProduction, httpOnly: true, - maxAge: 1000 * 60 * 60 * 1, // One hour + maxAge: 1000 * 60 * 60 * 24, // 24 hours }, }), ); diff --git a/src/presentation/controllers/user.controller.ts b/src/presentation/controllers/user.controller.ts index ad8c2a6..5dfda5b 100644 --- a/src/presentation/controllers/user.controller.ts +++ b/src/presentation/controllers/user.controller.ts @@ -1,4 +1,20 @@ -import { Controller } from '@nestjs/common'; +import { SessionData } from '@/types/session.types'; +import { UserDTO } from '@/presentation/dtos/user.dto'; +import { + Controller, + Get, + Session, + UnauthorizedException, +} from '@nestjs/common'; @Controller('user') -export class UserController {} +export class UserController { + @Get('profile') + async getProfile(@Session() session: SessionData): Promise<{ user: UserDTO }> { + if (!session || !session.user) { + throw new UnauthorizedException('No active session'); + } + + return { user: session.user }; + } +} From 0eb910feef39a66c7e31f22005c48f3e3da07b3e Mon Sep 17 00:00:00 2001 From: AlirizaSari Date: Tue, 16 Dec 2025 12:31:20 +0100 Subject: [PATCH 2/2] feat: implement user retrieval in UserService and UserController with error handling --- src/application/ports/user.port.ts | 7 +++- src/application/services/user.service.ts | 6 +++ .../controllers/user.controller.ts | 38 ++++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/application/ports/user.port.ts b/src/application/ports/user.port.ts index f08c99b..8198aa7 100644 --- a/src/application/ports/user.port.ts +++ b/src/application/ports/user.port.ts @@ -1 +1,6 @@ -export interface IUserService {} +import { Result } from '@/result'; +import { User } from '@/domain/user/user.model'; + +export interface IUserService { + findById(id: string): Promise>; +} diff --git a/src/application/services/user.service.ts b/src/application/services/user.service.ts index 9916d3f..bf0e4b1 100644 --- a/src/application/services/user.service.ts +++ b/src/application/services/user.service.ts @@ -1,6 +1,8 @@ import { type IUserRepository } from '@/domain/user/user-repository.interface'; import { IUserService } from '@/application/ports/user.port'; import { Inject, Injectable } from '@nestjs/common'; +import { Result } from '@/result'; +import { User } from '@/domain/user/user.model'; @Injectable() export class UserService implements IUserService { @@ -9,4 +11,8 @@ export class UserService implements IUserService { constructor(@Inject('REPO.USER') _userRepository: IUserRepository) { this.userRepository = _userRepository; } + + async findById(id: string): Promise> { + return this.userRepository.findById(id); + } } diff --git a/src/presentation/controllers/user.controller.ts b/src/presentation/controllers/user.controller.ts index 5dfda5b..5caca95 100644 --- a/src/presentation/controllers/user.controller.ts +++ b/src/presentation/controllers/user.controller.ts @@ -1,20 +1,54 @@ import { SessionData } from '@/types/session.types'; import { UserDTO } from '@/presentation/dtos/user.dto'; +import { IUserService } from '@/application/ports/user.port'; +import { User } from '@/domain/user/user.model'; +import { Result } from '@/result'; import { Controller, Get, + Inject, + NotFoundException, Session, UnauthorizedException, } from '@nestjs/common'; @Controller('user') export class UserController { + private readonly userService: IUserService; + + constructor(@Inject('SERVICE.USER') _userService: IUserService) { + this.userService = _userService; + } + @Get('profile') - async getProfile(@Session() session: SessionData): Promise<{ user: UserDTO }> { + async getProfile( + @Session() session: SessionData, + ): Promise<{ user: UserDTO }> { if (!session || !session.user) { throw new UnauthorizedException('No active session'); } - return { user: session.user }; + const result: Result = await this.userService.findById( + session.user.id, + ); + if (result._tag === 'Failure' || !result.data) { + throw new NotFoundException('User not found'); + } + + const freshUser = result.data; + session.user = freshUser; + + return { user: this.toUserDto(freshUser) }; + } + + private toUserDto(user: User): UserDTO { + const dto = new UserDTO(); + dto.id = user.id; + dto.name = user.name; + dto.email = user.email; + dto.role = user.role; + dto.createdAt = user.createdAt; + dto.updatedAt = user.updatedAt; + return dto; } }