From 62d9a9ab08ae0b4767f56c78a3684bb12ab7cd6b Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 16:51:00 +0100 Subject: [PATCH 01/20] implemented the Implement Secure Session and Token Management --- backend/src/auth/authConfig/jwt.config.ts | 5 +- .../src/auth/controllers/auth.controller.ts | 45 ++++- backend/src/auth/entities/session.entity.ts | 42 +++++ backend/src/auth/providers/auth.service.ts | 16 +- .../providers/generate-tokens.provider.ts | 6 +- .../auth/providers/refreshTokensProvider.ts | 66 ++------ .../src/auth/providers/sessions.provider.ts | 156 ++++++++++++++++++ .../src/auth/providers/sign-in.provider.ts | 34 ++-- 8 files changed, 286 insertions(+), 84 deletions(-) create mode 100644 backend/src/auth/entities/session.entity.ts create mode 100644 backend/src/auth/providers/sessions.provider.ts diff --git a/backend/src/auth/authConfig/jwt.config.ts b/backend/src/auth/authConfig/jwt.config.ts index 27809d47..a20a018c 100644 --- a/backend/src/auth/authConfig/jwt.config.ts +++ b/backend/src/auth/authConfig/jwt.config.ts @@ -7,6 +7,7 @@ export default registerAs('jwt', () => { googleClient_id: process.env.GOOGLE_CLIENT_ID, googleClient_secret: process.env.GOOGLE_CLIENT_SECRET, issuer: process.env.JWT_TOKEN_ISSUER ?? 'localhost', - ttl: parseInt(process.env.JWT_ACCESS_TOKEN_TTL ?? '3600'), + accessTokenTtl: parseInt(process.env.JWT_ACCESS_TOKEN_TTL ?? '3600'), // 1 hour default + refreshTokenTtl: parseInt(process.env.JWT_REFRESH_TOKEN_TTL ?? '604800'), // 7 days default }; -}); +}); \ No newline at end of file diff --git a/backend/src/auth/controllers/auth.controller.ts b/backend/src/auth/controllers/auth.controller.ts index 6978c77f..c3ec8277 100644 --- a/backend/src/auth/controllers/auth.controller.ts +++ b/backend/src/auth/controllers/auth.controller.ts @@ -7,16 +7,21 @@ import { Get, Query, Param, + Req, + UseGuards, } from '@nestjs/common'; import { Throttle } from '@nestjs/throttler'; import { LoginDto } from '../dtos/login.dto'; import { AuthService } from '../providers/auth.service'; import { RefreshTokenDto } from '../dtos/refreshTokenDto'; -import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { NonceResponseDto } from '../dtos/nonceResponse.dto'; import { StellarWalletLoginDto } from '../dtos/walletLogin.dto'; import { ResetPasswordDto } from '../dtos/reset-password.dto'; import { ForgotPasswordDto } from '../dtos/forgot-password.dto'; +import { AuthGuard } from '@nestjs/jwt'; +import { ActiveUser } from '../decorators/activeUser.decorator'; +import { ActiveInterface } from '../interfaces/activeInterface'; import { GuestSessionProvider } from '../providers/guest-session.provider'; import { ConvertGuestDto } from '../dtos/convert-guest.dto'; @@ -198,6 +203,42 @@ export class AuthController { } @Post('/reset-password/:token') + public async resetPassword( + @Param('token') token: string, + @Body() resetPasswordDto: ResetPasswordDto, + ) { + return await this.authservice.resetPassword(token, resetPasswordDto); + } + + @Post('/logout') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Logout current user and invalidate session' }) + @ApiResponse({ status: 200, description: 'Successfully logged out' }) + @ApiResponse({ status: 401, description: 'Invalid or missing refresh token' }) + public async logout(@Body() refreshTokenDto: RefreshTokenDto) { + return await this.authservice.logout(refreshTokenDto); + } + + @Post('/logout-all') + @UseGuards(AuthGuard) + @ApiBearerAuth() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Logout from all devices' }) + @ApiResponse({ status: 200, description: 'All sessions invalidated' }) + public async logoutAll(@ActiveUser() user: ActiveInterface) { + return await this.authservice.logoutAll(user.sub); + } + + @Get('/me') + @UseGuards(AuthGuard) + @ApiBearerAuth() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Get current authenticated user' }) + @ApiResponse({ status: 200, description: 'Current user data retrieved' }) + @ApiResponse({ status: 401, description: 'Unauthorized - invalid or missing token' }) + public async getCurrentUser(@ActiveUser() user: ActiveInterface) { + return await this.authservice.getCurrentUser(user.sub); + } @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Reset password with token', @@ -226,4 +267,4 @@ export class AuthController { ) { return await this.authservice.resetPassword(token, resetPasswordDto); } -} +} \ No newline at end of file diff --git a/backend/src/auth/entities/session.entity.ts b/backend/src/auth/entities/session.entity.ts new file mode 100644 index 00000000..3fd4c10f --- /dev/null +++ b/backend/src/auth/entities/session.entity.ts @@ -0,0 +1,42 @@ +import { + Column, + Entity, + PrimaryGeneratedColumn, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { User } from '../../users/user.entity'; + +@Entity() +export class Session { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + user: User; + + @Column() + userId: string; + + @Column({ unique: true }) + refreshTokenHash: string; + + @Column({ nullable: true }) + deviceInfo?: string; + + @Column({ nullable: true }) + ipAddress?: string; + + @Column({ type: 'timestamp' }) + expiresAt: Date; + + @Column({ default: true }) + isActive: boolean; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} \ No newline at end of file diff --git a/backend/src/auth/providers/auth.service.ts b/backend/src/auth/providers/auth.service.ts index 9e33cad6..ec6e070c 100644 --- a/backend/src/auth/providers/auth.service.ts +++ b/backend/src/auth/providers/auth.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common'; import { LoginDto } from '../dtos/login.dto'; import { SignInProvider } from './sign-in.provider'; import { ApiBody, ApiOperation } from '@nestjs/swagger'; @@ -12,6 +12,8 @@ import { ResetPasswordProvider } from './reset-password.provider'; import { ForgotPasswordDto } from '../dtos/forgot-password.dto'; import { ResetPasswordDto } from '../dtos/reset-password.dto'; import { NonceService } from './nonce.service'; +import { SessionsProvider } from './sessions.provider'; +import { UsersService } from '../../users/providers/users.service'; @Injectable() export class AuthService { @@ -45,6 +47,16 @@ export class AuthService { * inject nonceService */ private readonly nonceService: NonceService, + + /** + * Inject SessionsProvider for secure session management + */ + private readonly sessionsProvider: SessionsProvider, + + /** + * Inject UsersService to retrieve user data + */ + private readonly usersService: UsersService, ) {} public async SignIn(signInDto: LoginDto) { @@ -139,4 +151,4 @@ export class AuthService { resetPasswordDto, ); } -} +} \ No newline at end of file diff --git a/backend/src/auth/providers/generate-tokens.provider.ts b/backend/src/auth/providers/generate-tokens.provider.ts index 255941da..0147ef4d 100644 --- a/backend/src/auth/providers/generate-tokens.provider.ts +++ b/backend/src/auth/providers/generate-tokens.provider.ts @@ -72,12 +72,12 @@ export class GenerateTokensProvider { } const [accessToken, refreshToken] = await Promise.all([ - this.signToken(user.id, user.username, this.jwtConfiguration.ttl, { + this.signToken(user.id, user.username, this.jwtConfiguration.accessTokenTtl, { email: user.email, }), - this.signToken(user.id, user.username, this.jwtConfiguration.ttl), + this.signToken(user.id, user.username, this.jwtConfiguration.refreshTokenTtl), ]); return { accessToken, refreshToken, user }; } -} +} \ No newline at end of file diff --git a/backend/src/auth/providers/refreshTokensProvider.ts b/backend/src/auth/providers/refreshTokensProvider.ts index ddaaa8ae..133ef1dc 100644 --- a/backend/src/auth/providers/refreshTokensProvider.ts +++ b/backend/src/auth/providers/refreshTokensProvider.ts @@ -1,16 +1,7 @@ -import { - forwardRef, - Inject, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { RefreshTokenDto } from '../dtos/refreshTokenDto'; -import { JwtService } from '@nestjs/jwt'; -import { ConfigType } from '@nestjs/config'; -import jwtConfig from '../authConfig/jwt.config'; -import { GenerateTokensProvider } from './generate-tokens.provider'; import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger'; -import { UsersService } from '../../users/providers/users.service'; +import { SessionsProvider } from './sessions.provider'; /** * Refresh token provider class @@ -20,26 +11,9 @@ import { UsersService } from '../../users/providers/users.service'; export class RefreshTokensProvider { constructor( /** - * Injecting UserService repository + * Injecting SessionsProvider for secure session management */ - @Inject(forwardRef(() => UsersService)) - private readonly userService: UsersService, - - /** - * Injecting JwtService - */ - private readonly jwtService: JwtService, - - /** - * Injecting JWT Configuration - */ - @Inject(jwtConfig.KEY) - private readonly jwtConfiguration: ConfigType, - - /** - * Injecting GenerateTokensProvider - */ - private readonly generateTokenProvider: GenerateTokensProvider, + private readonly sessionsProvider: SessionsProvider, ) {} /** @@ -49,28 +23,16 @@ export class RefreshTokensProvider { */ @ApiOperation({ summary: 'Refresh authentication tokens' }) @ApiBody({ type: RefreshTokenDto }) - public async refreshTokens(refreshTokenDto: RefreshTokenDto) { - // Validate the refresh token using JWT - const payload = await this.jwtService.verifyAsync<{ sub: string }>( + public async refreshTokens( + refreshTokenDto: RefreshTokenDto, + deviceInfo?: string, + ipAddress?: string, + ) { + // Use sessions provider to validate and rotate the refresh token + return await this.sessionsProvider.refreshSession( refreshTokenDto.refreshToken, - { - secret: this.jwtConfiguration.secret, - audience: this.jwtConfiguration.audience, - issuer: this.jwtConfiguration.issuer, - }, + deviceInfo, + ipAddress, ); - - const sub = payload.sub; - - // Retrieve the user from the database - const user = await this.userService.findOneByGoogleId(sub); - - // inside refreshTokens - if (!user) { - throw new UnauthorizedException('Invalid refresh token'); - } - - // Generate new tokens - return await this.generateTokenProvider.generateTokens(user); } -} +} \ No newline at end of file diff --git a/backend/src/auth/providers/sessions.provider.ts b/backend/src/auth/providers/sessions.provider.ts new file mode 100644 index 00000000..a9577aec --- /dev/null +++ b/backend/src/auth/providers/sessions.provider.ts @@ -0,0 +1,156 @@ +import { + Injectable, + UnauthorizedException, + Inject, + forwardRef, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Session } from '../entities/session.entity'; +import { User } from '../../users/user.entity'; +import * as crypto from 'crypto'; +import { ConfigType } from '@nestjs/config'; +import jwtConfig from '../authConfig/jwt.config'; +import { GenerateTokensProvider } from './generate-tokens.provider'; +import { UsersService } from '../../users/providers/users.service'; + +@Injectable() +export class SessionsProvider { + constructor( + @InjectRepository(Session) + private readonly sessionRepository: Repository, + + @Inject(jwtConfig.KEY) + private readonly jwtConfiguration: ConfigType, + + private readonly generateTokensProvider: GenerateTokensProvider, + + @Inject(forwardRef(() => UsersService)) + private readonly usersService: UsersService, + ) {} + + /** + * Hash a refresh token for secure storage + */ + private hashRefreshToken(refreshToken: string): string { + return crypto.createHash('sha256').update(refreshToken).digest('hex'); + } + + /** + * Create a new session for a user + */ + public async createSession( + user: User, + deviceInfo?: string, + ipAddress?: string, + ): Promise<{ accessToken: string; refreshToken: string; user: User }> { + // Generate new tokens + const tokens = await this.generateTokensProvider.generateTokens(user); + + // Calculate refresh token expiration + const expiresAt = new Date(); + expiresAt.setSeconds( + expiresAt.getSeconds() + this.jwtConfiguration.refreshTokenTtl, + ); + + // Hash the refresh token before storing + const refreshTokenHash = this.hashRefreshToken(tokens.refreshToken); + + // Create and save the session + const session = this.sessionRepository.create({ + userId: user.id, + refreshTokenHash, + deviceInfo, + ipAddress, + expiresAt, + isActive: true, + }); + + await this.sessionRepository.save(session); + + return tokens; + } + + /** + * Validate and rotate refresh token (token rotation for security) + */ + public async refreshSession( + refreshToken: string, + deviceInfo?: string, + ipAddress?: string, + ): Promise<{ accessToken: string; refreshToken: string; user: User }> { + const refreshTokenHash = this.hashRefreshToken(refreshToken); + + // Find the session with matching hash that is still active and not expired + const session = await this.sessionRepository.findOne({ + where: { + refreshTokenHash, + isActive: true, + }, + relations: ['user'], + }); + + if (!session) { + throw new UnauthorizedException('Invalid refresh token'); + } + + if (new Date() > session.expiresAt) { + // Invalidate the expired session + session.isActive = false; + await this.sessionRepository.save(session); + throw new UnauthorizedException('Refresh token has expired'); + } + + // Invalidate the old session (token rotation) + session.isActive = false; + await this.sessionRepository.save(session); + + // Create a new session with new tokens + return this.createSession(session.user, deviceInfo, ipAddress); + } + + /** + * Invalidate a specific session (logout) + */ + public async invalidateSession(refreshToken: string): Promise { + const refreshTokenHash = this.hashRefreshToken(refreshToken); + const session = await this.sessionRepository.findOne({ + where: { refreshTokenHash }, + }); + + if (session) { + session.isActive = false; + await this.sessionRepository.save(session); + } + } + + /** + * Invalidate all sessions for a user (logout from all devices) + */ + public async invalidateAllUserSessions(userId: string): Promise { + await this.sessionRepository.update( + { userId, isActive: true }, + { isActive: false }, + ); + } + + /** + * Get all active sessions for a user + */ + public async getUserActiveSessions(userId: string): Promise { + return this.sessionRepository.find({ + where: { userId, isActive: true }, + order: { createdAt: 'DESC' }, + }); + } + + /** + * Clean up expired sessions (can be run as a cron job) + */ + public async cleanupExpiredSessions(): Promise { + await this.sessionRepository.update( + { expiresAt: new Date(), isActive: true }, + { isActive: false }, + ); + } +} \ No newline at end of file diff --git a/backend/src/auth/providers/sign-in.provider.ts b/backend/src/auth/providers/sign-in.provider.ts index e76ed4ce..767ba49d 100644 --- a/backend/src/auth/providers/sign-in.provider.ts +++ b/backend/src/auth/providers/sign-in.provider.ts @@ -5,12 +5,12 @@ import { RequestTimeoutException, UnauthorizedException, } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; import { ConfigType } from '@nestjs/config'; import { UsersService } from '../../users/providers/users.service'; import { HashingProvider } from './hashing.provider'; import jwtConfig from '../authConfig/jwt.config'; import { LoginDto } from '../dtos/login.dto'; +import { SessionsProvider } from './sessions.provider'; @Injectable() export class SignInProvider { @@ -22,19 +22,19 @@ export class SignInProvider { // injecting hashing dependency private readonly hashingProvider: HashingProvider, - // inject jwt service - private readonly jwtService: JwtService, - - // inject jwt + // inject jwt configuration @Inject(jwtConfig.KEY) private readonly jwtConfiguration: ConfigType, + + // inject sessions provider for secure session management + private readonly sessionsProvider: SessionsProvider, ) {} - public async SignIn(signInDto: LoginDto) { + public async SignIn(signInDto: LoginDto, deviceInfo?: string, ipAddress?: string) { // check if user exist in db // throw error if user doesnt exist const user = await this.userService.GetOneByEmail(signInDto.email); - // conpare password + // compare password let isCheckedPassword: boolean = false; try { @@ -47,7 +47,7 @@ export class SignInProvider { ); } catch (error) { throw new RequestTimeoutException(error, { - description: 'error connecting to the database', + description: 'error connecting to the database', }); } @@ -55,19 +55,7 @@ export class SignInProvider { throw new UnauthorizedException('email or password is incorrect'); } - const accessToken = await this.jwtService.signAsync( - { - sub: user.id, - email: user.email, - }, - { - audience: this.jwtConfiguration.audience, - issuer: this.jwtConfiguration.issuer, - expiresIn: this.jwtConfiguration.ttl, - }, - ); - - // login - return { accessToken }; + // Create a new secure session with access and refresh tokens + return await this.sessionsProvider.createSession(user, deviceInfo, ipAddress); } -} +} \ No newline at end of file From 2030e219f10efa5138e023b35ee72c22ba23fd39 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 16:51:04 +0100 Subject: [PATCH 02/20] implemented the Implement Secure Session and Token Management --- backend/src/auth/auth.module.ts | 4 ++- backend/src/auth/providers/auth.service.ts | 29 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 63707695..7ec65a44 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -27,6 +27,7 @@ import { GuestSessionProvider } from './providers/guest-session.provider'; imports: [ forwardRef(() => UsersModule), ConfigModule.forFeature(jwtConfig), + TypeOrmModule.forFeature([Session]), JwtModule.registerAsync(jwtConfig.asProvider()), ThrottlerModule.forRoot([ { @@ -49,6 +50,7 @@ import { GuestSessionProvider } from './providers/guest-session.provider'; ResetPasswordProvider, MailService, NonceService, + SessionsProvider, { provide: HashingProvider, // Use the abstract class as a token useClass: BcryptProvider, // Bind it to the concrete implementation @@ -67,4 +69,4 @@ import { GuestSessionProvider } from './providers/guest-session.provider'; NonceService, ], }) -export class AuthModule {} +export class AuthModule {} \ No newline at end of file diff --git a/backend/src/auth/providers/auth.service.ts b/backend/src/auth/providers/auth.service.ts index ec6e070c..c09cd1b0 100644 --- a/backend/src/auth/providers/auth.service.ts +++ b/backend/src/auth/providers/auth.service.ts @@ -151,4 +151,33 @@ export class AuthService { resetPasswordDto, ); } + + /** + * Logout current user by invalidating their refresh token + */ + public async logout(refreshTokenDto: RefreshTokenDto) { + await this.sessionsProvider.invalidateSession(refreshTokenDto.refreshToken); + return { message: 'Successfully logged out' }; + } + + /** + * Invalidate all sessions for a user (logout from all devices) + */ + public async logoutAll(userId: string) { + await this.sessionsProvider.invalidateAllUserSessions(userId); + return { message: 'All sessions invalidated successfully' }; + } + + /** + * Get current authenticated user data + */ + public async getCurrentUser(userId: string) { + const user = await this.usersService.findOneById(userId); + if (!user) { + throw new UnauthorizedException('User not found'); + } + // Return user without sensitive data + const { password, passwordResetToken, passwordResetExpires, ...userWithoutSensitiveData } = user; + return userWithoutSensitiveData; + } } \ No newline at end of file From 3fa1cfef768345bff1e418594f4fd076136acc09 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 16:58:50 +0100 Subject: [PATCH 03/20] implemented the Implement Secure Session and Token Management --- .../20260821000000-CreateSessionsTable.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 backend/src/database/migrations/20260821000000-CreateSessionsTable.ts diff --git a/backend/src/database/migrations/20260821000000-CreateSessionsTable.ts b/backend/src/database/migrations/20260821000000-CreateSessionsTable.ts new file mode 100644 index 00000000..02bea185 --- /dev/null +++ b/backend/src/database/migrations/20260821000000-CreateSessionsTable.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateSessionsTable20260821000000 implements MigrationInterface { + name = 'CreateSessionsTable20260821000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "session" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "userId" string NOT NULL, + "refreshTokenHash" VARCHAR NOT NULL UNIQUE, + "deviceInfo" VARCHAR, + "ipAddress" VARCHAR, + "expiresAt" TIMESTAMP NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("id"), + CONSTRAINT "FK_session_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE + ); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP TABLE "session"; + `); + } +} \ No newline at end of file From a1f84866c2109a7593865f8ea586ee7b9a5fdc94 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 16:58:58 +0100 Subject: [PATCH 04/20] implemented the Implement Secure Session and Token Management --- .../database/migrations/20260821000000-CreateSessionsTable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/database/migrations/20260821000000-CreateSessionsTable.ts b/backend/src/database/migrations/20260821000000-CreateSessionsTable.ts index 02bea185..8e00ca8e 100644 --- a/backend/src/database/migrations/20260821000000-CreateSessionsTable.ts +++ b/backend/src/database/migrations/20260821000000-CreateSessionsTable.ts @@ -7,7 +7,7 @@ export class CreateSessionsTable20260821000000 implements MigrationInterface { await queryRunner.query(` CREATE TABLE "session" ( "id" UUID NOT NULL DEFAULT uuid_generate_v4(), - "userId" string NOT NULL, + "userId" INTEGER NOT NULL, "refreshTokenHash" VARCHAR NOT NULL UNIQUE, "deviceInfo" VARCHAR, "ipAddress" VARCHAR, From 82287dbcce171c7d90cdcf29bc846087b5ac46ba Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:00:16 +0100 Subject: [PATCH 05/20] implemneted the Implement Secure Session and Token Management --- backend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index daa2f36e..cc0d7363 100644 --- a/backend/package.json +++ b/backend/package.json @@ -87,6 +87,7 @@ "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.7.3", - "typescript-eslint": "^8.20.0" + "typescript-eslint": "^8.20.0", + "yn": "^3.1.1" } } From 7027c5ab20895ca25a6eccbc13c4d009a3084119 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:00:56 +0100 Subject: [PATCH 06/20] implemneted the Implement Centralized API Error Handling --- package-lock.json | 113 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index db6cd8d2..e372e712 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,8 @@ "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.7.3", - "typescript-eslint": "^8.20.0" + "typescript-eslint": "^8.20.0", + "yn": "^3.1.1" } }, "backend/node_modules/@angular-devkit/core": { @@ -718,6 +719,16 @@ } } }, + "backend/node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "frontend": { "version": "0.1.0", "dependencies": { @@ -1583,6 +1594,30 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -3374,7 +3409,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3395,7 +3430,7 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -5165,6 +5200,34 @@ "node": ">= 6" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -6474,6 +6537,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -6693,6 +6769,13 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -8223,6 +8306,13 @@ "node": "*" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/cron": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.5.tgz", @@ -8657,6 +8747,16 @@ "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", "license": "MIT" }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -18160,6 +18260,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", From fec0b42c7cd1851232fbb63620947a3873136749 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:01:01 +0100 Subject: [PATCH 07/20] implemneted the Implement Centralized API Error Handling --- frontend/lib/features/auth/authSlice.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/lib/features/auth/authSlice.ts b/frontend/lib/features/auth/authSlice.ts index d465be3b..58e8672a 100644 --- a/frontend/lib/features/auth/authSlice.ts +++ b/frontend/lib/features/auth/authSlice.ts @@ -14,6 +14,7 @@ export interface User { export interface AuthState { user: User | null; token: string | null; + refreshToken: string | null; isAuthenticated: boolean; isLoading: boolean; error: string | null; @@ -24,6 +25,7 @@ export interface AuthState { const initialState: AuthState = { user: null, token: typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null, + refreshToken: typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null, isAuthenticated: typeof window !== 'undefined' ? !!localStorage.getItem('accessToken') : false, isLoading: false, error: null, @@ -231,4 +233,4 @@ export const selectToken = (state: { auth: AuthState }) => state.auth.token; export const selectIsRestoring = (state: { auth: AuthState }) => state.auth.isRestoring; // Reducer -export default authSlice.reducer; +export default authSlice.reducer; \ No newline at end of file From c1d70735f9c393603a55728cc5ec70faca793d95 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:01:12 +0100 Subject: [PATCH 08/20] implemneted the Implement Secure Session and Token Management --- frontend/lib/features/auth/authSlice.ts | 62 ++++++++++++++++++++----- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/frontend/lib/features/auth/authSlice.ts b/frontend/lib/features/auth/authSlice.ts index 58e8672a..3a26635d 100644 --- a/frontend/lib/features/auth/authSlice.ts +++ b/frontend/lib/features/auth/authSlice.ts @@ -44,18 +44,31 @@ export const loginStart = createAsyncThunk( export const restoreSession = createAsyncThunk( 'auth/restoreSession', - async (_, { rejectWithValue }) => { + async (_, { rejectWithValue, getState }) => { try { const token = localStorage.getItem('accessToken'); if (!token) { throw new Error('No token found'); } - // TODO: Validate token with backend and fetch user data - // For now, just return the token - return { token }; - } catch { + // Validate token with backend by fetching user data + const response = await fetch('/api/auth/me', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Invalid token'); + } + + const user = await response.json(); + return { token, user }; + } catch (error) { + // Clear invalid tokens localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); return rejectWithValue('Session expired'); } } @@ -63,16 +76,41 @@ export const restoreSession = createAsyncThunk( export const refreshToken = createAsyncThunk( 'auth/refreshToken', - async (_, { rejectWithValue }) => { + async (_, { rejectWithValue, getState }) => { try { - // TODO: Implement token refresh logic - const token = localStorage.getItem('accessToken'); - if (!token) { - throw new Error('No token to refresh'); + const refreshTokenValue = localStorage.getItem('refreshToken'); + if (!refreshTokenValue) { + throw new Error('No refresh token found'); + } + + // Call refresh token endpoint + const response = await fetch('/api/auth/refresh', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ refreshToken: refreshTokenValue }), + }); + + if (!response.ok) { + throw new Error('Failed to refresh token'); } - return { token }; - } catch { + + const data = await response.json(); + + // Update stored tokens + localStorage.setItem('accessToken', data.accessToken); + localStorage.setItem('refreshToken', data.refreshToken); + + return { + accessToken: data.accessToken, + refreshToken: data.refreshToken, + user: data.user + }; + } catch (error) { + // Clear invalid tokens localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); return rejectWithValue('Failed to refresh token'); } } From 3d3ce6bd5f8aa8a3f608eee9d1bb7283384cc592 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:01:28 +0100 Subject: [PATCH 09/20] implemneted the Implement Secure Session and Token Management --- frontend/lib/features/auth/authSlice.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/lib/features/auth/authSlice.ts b/frontend/lib/features/auth/authSlice.ts index 3a26635d..00b23611 100644 --- a/frontend/lib/features/auth/authSlice.ts +++ b/frontend/lib/features/auth/authSlice.ts @@ -121,16 +121,18 @@ const authSlice = createSlice({ name: 'auth', initialState, reducers: { - loginSuccess: (state, action: PayloadAction<{ user: User; token: string }>) => { + loginSuccess: (state, action: PayloadAction<{ user: User; token: string; refreshToken: string }>) => { state.user = action.payload.user; state.token = action.payload.token; + state.refreshToken = action.payload.refreshToken; state.isAuthenticated = true; state.isLoading = false; state.error = null; - // Store token in localStorage + // Store tokens in localStorage if (typeof window !== 'undefined') { localStorage.setItem('accessToken', action.payload.token); + localStorage.setItem('refreshToken', action.payload.refreshToken); } }, From 3af72b5c0a7e3f082446042e5ecf5528238b2e53 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:02:14 +0100 Subject: [PATCH 10/20] implemneted the Implement Secure Session and Token Management --- frontend/hooks/useAuth.ts | 3 ++- frontend/lib/features/auth/authSlice.ts | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/hooks/useAuth.ts b/frontend/hooks/useAuth.ts index 5b56979d..7226c900 100644 --- a/frontend/hooks/useAuth.ts +++ b/frontend/hooks/useAuth.ts @@ -8,6 +8,7 @@ import { selectAuthLoading, selectAuthError, selectToken, + selectRefreshToken, selectIsRestoring, loginSuccess, loginFailure, @@ -121,4 +122,4 @@ export function useAuthToken() { export function useIsRestoring() { return useAppSelector(selectIsRestoring); -} +} \ No newline at end of file diff --git a/frontend/lib/features/auth/authSlice.ts b/frontend/lib/features/auth/authSlice.ts index 00b23611..8f912247 100644 --- a/frontend/lib/features/auth/authSlice.ts +++ b/frontend/lib/features/auth/authSlice.ts @@ -152,13 +152,15 @@ const authSlice = createSlice({ logout: (state) => { state.user = null; state.token = null; + state.refreshToken = null; state.isAuthenticated = false; state.isLoading = false; state.error = null; - // Remove token from localStorage + // Remove tokens from localStorage if (typeof window !== 'undefined') { localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); } }, @@ -223,12 +225,15 @@ const authSlice = createSlice({ .addCase(restoreSession.fulfilled, (state, action) => { state.isRestoring = false; state.token = action.payload.token; + state.user = action.payload.user; state.isAuthenticated = true; }) .addCase(restoreSession.rejected, (state, action) => { state.isRestoring = false; state.token = null; + state.refreshToken = null; state.isAuthenticated = false; + state.user = null; state.error = action.payload as string || 'Failed to restore session'; }) @@ -239,12 +244,17 @@ const authSlice = createSlice({ }) .addCase(refreshToken.fulfilled, (state, action) => { state.isLoading = false; - state.token = action.payload.token; + state.token = action.payload.accessToken; + state.refreshToken = action.payload.refreshToken; + if (action.payload.user) { + state.user = action.payload.user; + } state.isAuthenticated = true; }) .addCase(refreshToken.rejected, (state, action) => { state.isLoading = false; state.token = null; + state.refreshToken = null; state.isAuthenticated = false; state.user = null; state.error = action.payload as string || 'Failed to refresh token'; @@ -270,6 +280,7 @@ export const selectIsAuthenticated = (state: { auth: AuthState }) => state.auth. export const selectAuthLoading = (state: { auth: AuthState }) => state.auth.isLoading; export const selectAuthError = (state: { auth: AuthState }) => state.auth.error; export const selectToken = (state: { auth: AuthState }) => state.auth.token; +export const selectRefreshToken = (state: { auth: AuthState }) => state.auth.refreshToken; export const selectIsRestoring = (state: { auth: AuthState }) => state.auth.isRestoring; // Reducer From 3fa20f30aafae9c47238e6b1356787af27f37b42 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:02:17 +0100 Subject: [PATCH 11/20] implemneted the Implement Secure Session and Token Management --- frontend/hooks/useAuth.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/hooks/useAuth.ts b/frontend/hooks/useAuth.ts index 7226c900..ab98a5ae 100644 --- a/frontend/hooks/useAuth.ts +++ b/frontend/hooks/useAuth.ts @@ -33,6 +33,7 @@ export function useAuth() { const isLoading = useAppSelector(selectAuthLoading); const error = useAppSelector(selectAuthError); const token = useAppSelector(selectToken); + const refreshTokenValue = useAppSelector(selectRefreshToken); const isRestoring = useAppSelector(selectIsRestoring); // Actions From 86d2d733d215c79b94d548a95e28e4b1fac10283 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:02:57 +0100 Subject: [PATCH 12/20] implemneted the Implement Secure Session and Token Management --- frontend/hooks/useAuth.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/frontend/hooks/useAuth.ts b/frontend/hooks/useAuth.ts index ab98a5ae..43dab238 100644 --- a/frontend/hooks/useAuth.ts +++ b/frontend/hooks/useAuth.ts @@ -45,9 +45,26 @@ export function useAuth() { dispatch(loginFailure(error)); }, [dispatch]); - const handleLogout = useCallback(() => { + const handleLogout = useCallback(async () => { + if (refreshTokenValue) { + try { + // Call backend logout endpoint to invalidate the session + await fetch('/api/auth/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ refreshToken: refreshTokenValue }), + }); + } catch (error) { + console.error('Logout error:', error); + } + } + // Clear local state regardless of backend response dispatch(logout()); - }, [dispatch]); + // Redirect to signin page + window.location.href = '/auth/signin'; + }, [dispatch, refreshTokenValue]); const handleClearError = useCallback(() => { dispatch(clearError()); @@ -84,6 +101,7 @@ export function useAuth() { isLoading, error, token, + refreshToken: refreshTokenValue, isRestoring, // Actions @@ -121,6 +139,10 @@ export function useAuthToken() { return useAppSelector(selectToken); } +export function useRefreshToken() { + return useAppSelector(selectRefreshToken); +} + export function useIsRestoring() { return useAppSelector(selectIsRestoring); } \ No newline at end of file From a699b1ab360d4d2367bcbe4a2f2637d1f24dd109 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:03:21 +0100 Subject: [PATCH 13/20] implemneted the Implement Secure Session and Token Management --- frontend/app/auth/signin/page.tsx | 11 ++++------- frontend/components/ClientLayout.tsx | 11 +++++++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/frontend/app/auth/signin/page.tsx b/frontend/app/auth/signin/page.tsx index 3c1e3ea9..d8fd1443 100644 --- a/frontend/app/auth/signin/page.tsx +++ b/frontend/app/auth/signin/page.tsx @@ -155,18 +155,15 @@ const SignInPage = () => { // Parse JSON only if response is ok const data = await response.json(); - if (data.accessToken) { - // Store token in localStorage - localStorage.setItem('accessToken', data.accessToken); - - // Update Redux state + if (data.accessToken && data.refreshToken) { + // Update Redux state with both tokens const user = { id: data.user?.id || formData.username, email: formData.username, username: data.user?.username || formData.username.split('@')[0], }; - loginSuccess(user, data.accessToken); + loginSuccess(user, data.accessToken, data.refreshToken); // Show success toast showSuccess('Login Successful', 'Welcome back!'); @@ -367,4 +364,4 @@ const SignInPage = () => { ); }; -export default SignInPage; +export default SignInPage; \ No newline at end of file diff --git a/frontend/components/ClientLayout.tsx b/frontend/components/ClientLayout.tsx index afc94faa..74f14ec1 100644 --- a/frontend/components/ClientLayout.tsx +++ b/frontend/components/ClientLayout.tsx @@ -1,9 +1,10 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import SideNav from "@/components/SideNav"; import { Menu } from "lucide-react"; import ErrorBoundary from "@/components/error/ErrorBoundary"; +import { useAuth } from "@/hooks/useAuth"; export default function ClientLayout({ children, @@ -11,6 +12,12 @@ export default function ClientLayout({ children: React.ReactNode; }) { const [sidebarOpen, setSidebarOpen] = useState(false); + const { restoreSession } = useAuth(); + + // Restore session on app initialization + useEffect(() => { + restoreSession(); + }, [restoreSession]); return (
@@ -31,4 +38,4 @@ export default function ClientLayout({
); -} +} \ No newline at end of file From 5b719c2aedc1c4a7dd9d4a4cb7eb932cf4ba1cbd Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:04:01 +0100 Subject: [PATCH 14/20] implemneted the Implement Secure Session and Token Management --- frontend/components/ClientLayout.tsx | 16 ++++++++++++++++ frontend/components/SideNav.tsx | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/components/ClientLayout.tsx b/frontend/components/ClientLayout.tsx index 74f14ec1..80f33071 100644 --- a/frontend/components/ClientLayout.tsx +++ b/frontend/components/ClientLayout.tsx @@ -19,6 +19,22 @@ export default function ClientLayout({ restoreSession(); }, [restoreSession]); + // Set up automatic token refresh + const { refreshToken: refreshTokenAction, isAuthenticated, token } = useAuth(); + + useEffect(() => { + if (!isAuthenticated || !token) return; + + // Refresh token 5 minutes before it expires (assuming 1 hour expiration) + const REFRESH_INTERVAL = 55 * 60 * 1000; // 55 minutes + + const intervalId = setInterval(() => { + refreshTokenAction(); + }, REFRESH_INTERVAL); + + return () => clearInterval(intervalId); + }, [isAuthenticated, token, refreshTokenAction]); + return (
setSidebarOpen(false)} /> diff --git a/frontend/components/SideNav.tsx b/frontend/components/SideNav.tsx index 5b1a9197..9921a201 100644 --- a/frontend/components/SideNav.tsx +++ b/frontend/components/SideNav.tsx @@ -2,7 +2,8 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { Home, Trophy, User, Bell } from "lucide-react"; +import { Home, Trophy, User, Bell, LogOut } from "lucide-react"; +import { useAuth } from "@/hooks/useAuth"; const navItems = [ { label: "Home", href: "/dashboard", icon: Home }, @@ -14,6 +15,7 @@ const navItems = [ const SideNav = ({ open, onClose }: { open: boolean; onClose: () => void }) => { const pathname = usePathname(); + const { logout, isAuthenticated } = useAuth(); return (
From d1e22845f79867c8bb17dfbf1472dde14425c553 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:05:24 +0100 Subject: [PATCH 16/20] implemneted the Implement Secure Session and Token Management --- backend/src/auth/auth.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 7ec65a44..0e4df693 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -1,4 +1,5 @@ import { forwardRef, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthService } from './providers/auth.service'; import { UsersModule } from '../users/users.module'; import { SignInProvider } from './providers/sign-in.provider'; @@ -20,7 +21,8 @@ import { ForgotPasswordProvider } from './providers/forgot-password.provider'; import { ResetPasswordProvider } from './providers/reset-password.provider'; import { MailService } from './providers/mail.service'; import { NonceService } from './providers/nonce.service'; - +import { Session } from './entities/session.entity'; +import { SessionsProvider } from './providers/sessions.provider'; import { GuestSessionProvider } from './providers/guest-session.provider'; @Module({ From adf7b005ec89a5b973092110a58aba7051302b3e Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:06:56 +0100 Subject: [PATCH 17/20] implemneted the Implement Secure Session and Token Management --- backend/src/auth/controllers/auth.controller.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/auth/controllers/auth.controller.ts b/backend/src/auth/controllers/auth.controller.ts index c3ec8277..fc41efb3 100644 --- a/backend/src/auth/controllers/auth.controller.ts +++ b/backend/src/auth/controllers/auth.controller.ts @@ -19,9 +19,9 @@ import { NonceResponseDto } from '../dtos/nonceResponse.dto'; import { StellarWalletLoginDto } from '../dtos/walletLogin.dto'; import { ResetPasswordDto } from '../dtos/reset-password.dto'; import { ForgotPasswordDto } from '../dtos/forgot-password.dto'; -import { AuthGuard } from '@nestjs/jwt'; +import { AuthGuard } from '@nestjs/passport'; import { ActiveUser } from '../decorators/activeUser.decorator'; -import { ActiveInterface } from '../interfaces/activeInterface'; +import { ActiveUserData } from '../interfaces/activeInterface'; import { GuestSessionProvider } from '../providers/guest-session.provider'; import { ConvertGuestDto } from '../dtos/convert-guest.dto'; @@ -220,23 +220,23 @@ export class AuthController { } @Post('/logout-all') - @UseGuards(AuthGuard) + @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Logout from all devices' }) @ApiResponse({ status: 200, description: 'All sessions invalidated' }) - public async logoutAll(@ActiveUser() user: ActiveInterface) { + public async logoutAll(@ActiveUser() user: ActiveUserData) { return await this.authservice.logoutAll(user.sub); } @Get('/me') - @UseGuards(AuthGuard) + @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get current authenticated user' }) @ApiResponse({ status: 200, description: 'Current user data retrieved' }) @ApiResponse({ status: 401, description: 'Unauthorized - invalid or missing token' }) - public async getCurrentUser(@ActiveUser() user: ActiveInterface) { + public async getCurrentUser(@ActiveUser() user: ActiveUserData) { return await this.authservice.getCurrentUser(user.sub); } @HttpCode(HttpStatus.OK) From 3a023d8405fd97fe3e9fa7c1084a8854495a06b4 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Fri, 21 Aug 2026 17:09:11 +0100 Subject: [PATCH 18/20] implemneted the Implement Secure Session and Token Management --- .../src/auth/controllers/auth.controller.ts | 28 ------------------- .../auth/providers/wallet-login.provider.ts | 4 +-- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/backend/src/auth/controllers/auth.controller.ts b/backend/src/auth/controllers/auth.controller.ts index fc41efb3..d025162c 100644 --- a/backend/src/auth/controllers/auth.controller.ts +++ b/backend/src/auth/controllers/auth.controller.ts @@ -239,32 +239,4 @@ export class AuthController { public async getCurrentUser(@ActiveUser() user: ActiveUserData) { return await this.authservice.getCurrentUser(user.sub); } - @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: 'Reset password with token', - description: 'Resets user password using the token from email', - }) - @ApiResponse({ - status: 200, - description: 'Password reset successfully', - schema: { - type: 'object', - properties: { - message: { - type: 'string', - example: 'Password has been reset successfully', - }, - }, - }, - }) - @ApiResponse({ - status: 400, - description: 'Invalid or expired token', - }) - public async resetPassword( - @Param('token') token: string, - @Body() resetPasswordDto: ResetPasswordDto, - ) { - return await this.authservice.resetPassword(token, resetPasswordDto); - } } \ No newline at end of file diff --git a/backend/src/auth/providers/wallet-login.provider.ts b/backend/src/auth/providers/wallet-login.provider.ts index f2b93f90..def524f0 100644 --- a/backend/src/auth/providers/wallet-login.provider.ts +++ b/backend/src/auth/providers/wallet-login.provider.ts @@ -88,7 +88,7 @@ export class StellarWalletLoginProvider { { audience: this.jwtConfiguration.audience, issuer: this.jwtConfiguration.issuer, - expiresIn: this.jwtConfiguration.ttl, + expiresIn: this.jwtConfiguration.accessTokenTtl, }, ); @@ -180,4 +180,4 @@ export class StellarWalletLoginProvider { ); } } -} +} \ No newline at end of file From fcc38150ba2d43783fb9558d1f60f11985c23919 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Mon, 24 Aug 2026 10:18:44 +0000 Subject: [PATCH 19/20] run build --- frontend/hooks/useAuth.ts | 10 +++++----- package-lock.json | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/frontend/hooks/useAuth.ts b/frontend/hooks/useAuth.ts index 43dab238..49a74c1d 100644 --- a/frontend/hooks/useAuth.ts +++ b/frontend/hooks/useAuth.ts @@ -37,9 +37,9 @@ export function useAuth() { const isRestoring = useAppSelector(selectIsRestoring); // Actions - const handleLoginSuccess = useCallback((user: User, token: string) => { - dispatch(loginSuccess({ user, token })); - }, [dispatch]); + const handleLoginSuccess = useCallback((user: User, token: string, refreshToken?: string) => { + dispatch(loginSuccess({ user, token, refreshToken: refreshToken ?? refreshTokenValue ?? '' })); + }, [dispatch, refreshTokenValue]); const handleLoginFailure = useCallback((error: string) => { dispatch(loginFailure(error)); @@ -101,9 +101,9 @@ export function useAuth() { isLoading, error, token, - refreshToken: refreshTokenValue, + refreshTokenValue, isRestoring, - + // Actions loginSuccess: handleLoginSuccess, loginFailure: handleLoginFailure, diff --git a/package-lock.json b/package-lock.json index e372e712..d276245c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -248,7 +248,7 @@ }, "backend/node_modules/@swc/core": { "version": "1.13.5", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4691,6 +4691,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4707,6 +4708,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4723,6 +4725,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4739,6 +4742,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4755,6 +4759,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4771,6 +4776,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4787,6 +4793,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4803,6 +4810,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4819,6 +4827,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4835,6 +4844,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4848,7 +4858,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@swc/helpers": { @@ -4864,7 +4874,7 @@ "version": "0.1.25", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" From 47d2ab83346071e85999180149eaf8e63c59e863 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Mon, 24 Aug 2026 10:30:22 +0000 Subject: [PATCH 20/20] implemeneted the build --- .../src/auth/providers/sign-in.provider.ts | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/backend/src/auth/providers/sign-in.provider.ts b/backend/src/auth/providers/sign-in.provider.ts index c1504b2e..5dccb105 100644 --- a/backend/src/auth/providers/sign-in.provider.ts +++ b/backend/src/auth/providers/sign-in.provider.ts @@ -5,13 +5,11 @@ import { RequestTimeoutException, UnauthorizedException, } from '@nestjs/common'; -import { ConfigType } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { UsersService } from '../../users/providers/users.service'; import { User } from '../../users/user.entity'; import { HashingProvider } from './hashing.provider'; -import jwtConfig from '../authConfig/jwt.config'; import { LoginDto } from '../dtos/login.dto'; import { SessionsProvider } from './sessions.provider'; @@ -29,10 +27,6 @@ export class SignInProvider { // injecting hashing dependency private readonly hashingProvider: HashingProvider, - // inject jwt configuration - @Inject(jwtConfig.KEY) - private readonly jwtConfiguration: ConfigType, - // inject sessions provider for secure session management private readonly sessionsProvider: SessionsProvider, ) {} @@ -74,19 +68,7 @@ export class SignInProvider { }); } - const accessToken = await this.jwtService.signAsync( - { - sub: user.id, - email: user.email, - }, - { - audience: this.jwtConfiguration.audience, - issuer: this.jwtConfiguration.issuer, - expiresIn: this.jwtConfiguration.ttl, - }, - ); - - // login - return { accessToken }; + // Create a secure session and return the tokens expected by the frontend + return await this.sessionsProvider.createSession(user); } } \ No newline at end of file