Compare commits
No commits in common. "46f69e1c53dcd4d64394f9088fb9f0bb3221e0c9" and "ab1368553fb777179d4687b4e53cf0b9d6d79b9d" have entirely different histories.
46f69e1c53
...
ab1368553f
|
@ -35,22 +35,19 @@
|
||||||
"argon2": "^0.40.1",
|
"argon2": "^0.40.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"cookie-parser": "^1.4.6",
|
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"swagger-ui-express": "^5.0.0",
|
"swagger-ui-express": "^5.0.0",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20"
|
||||||
"uuid": "^9.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
"@types/argon2": "^0.15.0",
|
"@types/argon2": "^0.15.0",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
|
|
|
@ -41,9 +41,6 @@ dependencies:
|
||||||
class-validator:
|
class-validator:
|
||||||
specifier: ^0.14.1
|
specifier: ^0.14.1
|
||||||
version: 0.14.1
|
version: 0.14.1
|
||||||
cookie-parser:
|
|
||||||
specifier: ^1.4.6
|
|
||||||
version: 1.4.6
|
|
||||||
passport:
|
passport:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
|
@ -65,9 +62,6 @@ dependencies:
|
||||||
typeorm:
|
typeorm:
|
||||||
specifier: ^0.3.20
|
specifier: ^0.3.20
|
||||||
version: 0.3.20(pg@8.11.5)(ts-node@10.9.2)
|
version: 0.3.20(pg@8.11.5)(ts-node@10.9.2)
|
||||||
uuid:
|
|
||||||
specifier: ^9.0.1
|
|
||||||
version: 9.0.1
|
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@nestjs/cli':
|
'@nestjs/cli':
|
||||||
|
@ -82,9 +76,6 @@ devDependencies:
|
||||||
'@types/argon2':
|
'@types/argon2':
|
||||||
specifier: ^0.15.0
|
specifier: ^0.15.0
|
||||||
version: 0.15.0
|
version: 0.15.0
|
||||||
'@types/cookie-parser':
|
|
||||||
specifier: ^1.4.7
|
|
||||||
version: 1.4.7
|
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^4.17.17
|
specifier: ^4.17.17
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
@ -1297,12 +1288,6 @@ packages:
|
||||||
'@types/node': 20.12.4
|
'@types/node': 20.12.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/cookie-parser@1.4.7:
|
|
||||||
resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==}
|
|
||||||
dependencies:
|
|
||||||
'@types/express': 4.17.21
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/cookiejar@2.1.5:
|
/@types/cookiejar@2.1.5:
|
||||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -2252,22 +2237,9 @@ packages:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/cookie-parser@1.4.6:
|
|
||||||
resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==}
|
|
||||||
engines: {node: '>= 0.8.0'}
|
|
||||||
dependencies:
|
|
||||||
cookie: 0.4.1
|
|
||||||
cookie-signature: 1.0.6
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/cookie-signature@1.0.6:
|
/cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
/cookie@0.4.1:
|
|
||||||
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/cookie@0.6.0:
|
/cookie@0.6.0:
|
||||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { CorsMiddleware } from './middleware/cors-middleware/cors.middlware';
|
||||||
import { CspMiddleware } from './middleware/csp-middleware/csp.middleware';
|
import { CspMiddleware } from './middleware/csp-middleware/csp.middleware';
|
||||||
import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware';
|
import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware';
|
||||||
import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware';
|
import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware';
|
||||||
|
@ -34,8 +35,8 @@ export class AppModule {
|
||||||
.apply(
|
.apply(
|
||||||
CspMiddleware,
|
CspMiddleware,
|
||||||
SecurityHeadersMiddleware,
|
SecurityHeadersMiddleware,
|
||||||
HttpsRedirectMiddleware
|
HttpsRedirectMiddleware,
|
||||||
//CorsMiddleware
|
CorsMiddleware
|
||||||
)
|
)
|
||||||
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from './user-credentials.entity';
|
export * from './user-credentials.entity';
|
||||||
export * from './user-data.entity';
|
export * from './user-data.entity';
|
||||||
export * from './email-verification.entity';
|
export * from './email-verification.entity';
|
||||||
export * from './session.entity';
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import { UserCredentials } from './user-credentials.entity';
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
export class Session {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
public id: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
public sessionId: string;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp' })
|
|
||||||
public expiresAt: Date;
|
|
||||||
|
|
||||||
@Column({})
|
|
||||||
public userAgent: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => UserCredentials, (userCredentials) => userCredentials.id, {
|
|
||||||
nullable: false,
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'userCredentialsId' })
|
|
||||||
public userCredentials: UserCredentials['id'];
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
public createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
public updatedAt: Date;
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@ export class UserCredentials {
|
||||||
public hash: string;
|
public hash: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public refreshToken?: string;
|
public hashedRt?: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { join } from 'path';
|
||||||
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
import * as cookieParser from 'cookie-parser';
|
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
@ -31,10 +30,6 @@ async function setupSwagger(app: INestApplication): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupCookieParser(app: INestApplication): Promise<void> {
|
|
||||||
app.use(cookieParser());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupPrefix(app: INestApplication): Promise<void> {
|
async function setupPrefix(app: INestApplication): Promise<void> {
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api');
|
||||||
}
|
}
|
||||||
|
@ -46,7 +41,6 @@ async function setupClassValidator(app: INestApplication): Promise<void> {
|
||||||
async function bootstrap(): Promise<void> {
|
async function bootstrap(): Promise<void> {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
await setupCookieParser(app);
|
|
||||||
await setupSwagger(app);
|
await setupSwagger(app);
|
||||||
await setupPrefix(app);
|
await setupPrefix(app);
|
||||||
await setupClassValidator(app);
|
await setupClassValidator(app);
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Session, UserCredentials } from 'src/entities';
|
import { UserCredentials } from 'src/entities';
|
||||||
|
|
||||||
import { SendgridModule } from '../sendgrid-module/sendgrid.module';
|
import { SendgridModule } from '../sendgrid-module/sendgrid.module';
|
||||||
import { UserModule } from '../user-module/user.module';
|
import { UserModule } from '../user-module/user.module';
|
||||||
import { VerifyModule } from '../verify-module/verify.module';
|
import { VerifyModule } from '../verify-module/verify.module';
|
||||||
|
|
||||||
import { AuthController } from './controller/auth.controller';
|
import { AuthController } from './controller/auth.controller';
|
||||||
import { SessionRepository } from './repositories/session.repository';
|
|
||||||
import { UserCredentialsRepository } from './repositories/user-credentials.repository';
|
import { UserCredentialsRepository } from './repositories/user-credentials.repository';
|
||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from './services/auth.service';
|
||||||
import { SessionService } from './services/session.service';
|
|
||||||
import { TokenManagementService } from './services/token-management.service';
|
import { TokenManagementService } from './services/token-management.service';
|
||||||
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
||||||
|
|
||||||
|
@ -21,14 +19,12 @@ import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
||||||
SendgridModule,
|
SendgridModule,
|
||||||
VerifyModule,
|
VerifyModule,
|
||||||
JwtModule.register({}),
|
JwtModule.register({}),
|
||||||
TypeOrmModule.forFeature([UserCredentials, Session]),
|
TypeOrmModule.forFeature([UserCredentials]),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AuthService,
|
AuthService,
|
||||||
SessionService,
|
|
||||||
TokenManagementService,
|
TokenManagementService,
|
||||||
UserCredentialsRepository,
|
UserCredentialsRepository,
|
||||||
SessionRepository,
|
|
||||||
AccessTokenStrategy,
|
AccessTokenStrategy,
|
||||||
RefreshTokenStrategy,
|
RefreshTokenStrategy,
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
//import { JwtPayloadWithRefreshToken } from 'src/modules/auth-module/models/types';
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { JwtPayloadWithRefreshToken } from 'src/modules/auth-module/models/types';
|
||||||
|
|
||||||
// export const GetCurrentUser = createParamDecorator(
|
export const GetCurrentUser = createParamDecorator(
|
||||||
// (
|
(
|
||||||
// data: keyof JwtPayloadWithRefreshToken | undefined,
|
data: keyof JwtPayloadWithRefreshToken | undefined,
|
||||||
// context: ExecutionContext
|
context: ExecutionContext
|
||||||
// ) => {
|
) => {
|
||||||
// const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
// if (!data) return request.user;
|
if (!data) return request.user;
|
||||||
// return request.user[data];
|
return request.user[data];
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
export * from './get-user-id.decorator';
|
export * from './get-user-id.decorator';
|
||||||
// export * from './get-user.decorator';
|
export * from './get-user.decorator';
|
||||||
|
|
|
@ -4,19 +4,14 @@ import {
|
||||||
Body,
|
Body,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Res,
|
UseGuards,
|
||||||
Req,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiCreatedResponse, ApiHeader, ApiTags } from '@nestjs/swagger';
|
||||||
import { Response, Request } from 'express';
|
|
||||||
import { Public } from 'src/shared/decorator';
|
import { Public } from 'src/shared/decorator';
|
||||||
|
|
||||||
import { GetCurrentUserId } from '../common/decorators';
|
import { GetCurrentUser, GetCurrentUserId } from '../common/decorators';
|
||||||
import {
|
import { RefreshTokenGuard } from '../common/guards';
|
||||||
AccessTokenDto,
|
import { TokensDto, UserCredentialsDto } from '../models/dto';
|
||||||
LoginResponseDto,
|
|
||||||
UserCredentialsDto,
|
|
||||||
} from '../models/dto';
|
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
@ApiTags('Authentication')
|
@ApiTags('Authentication')
|
||||||
|
@ -26,50 +21,59 @@ export class AuthController {
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
@ApiCreatedResponse({
|
||||||
description: 'User signed up successfully',
|
description: 'User signed up successfully',
|
||||||
type: LoginResponseDto,
|
type: TokensDto,
|
||||||
})
|
})
|
||||||
@Public()
|
@Public()
|
||||||
@Post('signup')
|
@Post('signup')
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
public async signup(
|
public async signup(
|
||||||
@Body() userCredentials: UserCredentialsDto
|
@Body() userCredentials: UserCredentialsDto
|
||||||
): Promise<LoginResponseDto> {
|
): Promise<TokensDto> {
|
||||||
return this.authService.signup(userCredentials);
|
return this.authService.signup(userCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
@ApiCreatedResponse({
|
||||||
description: 'User signin successfully',
|
description: 'User signin successfully',
|
||||||
type: LoginResponseDto,
|
type: TokensDto,
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Public()
|
@Public()
|
||||||
@Post('signin')
|
@Post('signin')
|
||||||
public async signin(
|
|
||||||
@Res({ passthrough: true }) response: Response,
|
|
||||||
@Req() request: Request,
|
|
||||||
@Body() userCredentials: UserCredentialsDto
|
|
||||||
): Promise<LoginResponseDto> {
|
|
||||||
return await this.authService.signin(userCredentials, response, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
|
||||||
description: 'User tokens refreshed successfully',
|
|
||||||
type: AccessTokenDto,
|
|
||||||
})
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Public()
|
public async signin(
|
||||||
@Post('refresh')
|
@Body() userCredentials: UserCredentialsDto
|
||||||
public async refreshToken(@Req() request: Request): Promise<AccessTokenDto> {
|
): Promise<TokensDto> {
|
||||||
return await this.authService.refresh(request);
|
return this.authService.signin(userCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
@ApiCreatedResponse({
|
||||||
description: 'User signed out successfully',
|
description: 'User signed out successfully',
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Post('logout')
|
@Post('logout')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
public async logout(@GetCurrentUserId() userId: string): Promise<boolean> {
|
public async logout(@GetCurrentUserId() userId: string): Promise<boolean> {
|
||||||
return this.authService.logout(userId);
|
return this.authService.logout(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiHeader({
|
||||||
|
name: 'Authorization',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
example: 'Bearer <refresh_token>',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiCreatedResponse({
|
||||||
|
description: 'User tokens refreshed successfully',
|
||||||
|
type: TokensDto,
|
||||||
|
})
|
||||||
|
@Public()
|
||||||
|
@UseGuards(RefreshTokenGuard)
|
||||||
|
@Post('refresh')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
public async refresh(
|
||||||
|
@GetCurrentUserId() userId: string,
|
||||||
|
@GetCurrentUser('refresh_token') refresh_token: string
|
||||||
|
): Promise<TokensDto> {
|
||||||
|
return this.authService.refresh(userId, refresh_token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export * from './user-credentials.dto';
|
export * from './user-credentials.dto';
|
||||||
export * from './login-response.dto';
|
export * from './tokens.dto';
|
||||||
export * from './access-token.dto';
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class LoginResponseDto {
|
|
||||||
@ApiProperty({
|
|
||||||
title: 'Access token',
|
|
||||||
description: 'Access token',
|
|
||||||
example: 'eyJhbGci',
|
|
||||||
})
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
public access_token: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
title: 'Email',
|
|
||||||
description: 'User Email',
|
|
||||||
example: 'foo@bar.de',
|
|
||||||
})
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
@IsEmail()
|
|
||||||
public email: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
title: 'User ID',
|
|
||||||
description: 'User ID',
|
|
||||||
})
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
@IsEmail()
|
|
||||||
public userId: string;
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class AccessTokenDto {
|
export class TokensDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
title: 'Access token',
|
title: 'Access token',
|
||||||
description: 'Access token',
|
description: 'Access token',
|
||||||
|
@ -10,4 +10,13 @@ export class AccessTokenDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
public access_token: string;
|
public access_token: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
title: 'Refresh token',
|
||||||
|
description: 'Refresh token',
|
||||||
|
example: 'eyJhbGci',
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
public refresh_token: string;
|
||||||
}
|
}
|
|
@ -1,4 +1,2 @@
|
||||||
export * from './jwt-payload.type';
|
export * from './jwt-payload.type';
|
||||||
// export * from './jwt-payload-with-refresh-token.type';
|
export * from './jwt-payload-with-refresh-token.type';
|
||||||
export * from './token-payload.type';
|
|
||||||
export * from './tokens.type';
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
// import { JwtPayload } from './jwt-payload.type';
|
import { JwtPayload } from './jwt-payload.type';
|
||||||
|
|
||||||
// export type JwtPayloadWithRefreshToken = JwtPayload & { refresh_token: string };
|
export type JwtPayloadWithRefreshToken = JwtPayload & { refresh_token: string };
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
export type TokenPayload = {
|
|
||||||
sub: string;
|
|
||||||
email: string;
|
|
||||||
iat: number;
|
|
||||||
exp: number;
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
export type Tokens = {
|
|
||||||
access_token: string;
|
|
||||||
refresh_token: string;
|
|
||||||
};
|
|
|
@ -1,101 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Response } from 'express';
|
|
||||||
import { Session } from 'src/entities';
|
|
||||||
import { LessThan, Repository } from 'typeorm';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SessionRepository {
|
|
||||||
public constructor(
|
|
||||||
@InjectRepository(Session)
|
|
||||||
private sessionRepository: Repository<Session>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async createSession(
|
|
||||||
userId: string,
|
|
||||||
userAgent: string
|
|
||||||
): Promise<Session> {
|
|
||||||
const sessionId = uuidv4();
|
|
||||||
const expirationDate = new Date();
|
|
||||||
|
|
||||||
expirationDate.setHours(expirationDate.getHours() + 1);
|
|
||||||
const session = this.sessionRepository.create({
|
|
||||||
userCredentials: userId,
|
|
||||||
sessionId,
|
|
||||||
expiresAt: expirationDate,
|
|
||||||
userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.sessionRepository.save(session);
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async findSessionBySessionId(sessionId: string): Promise<Session> {
|
|
||||||
return await this.sessionRepository.findOne({
|
|
||||||
where: { sessionId: sessionId },
|
|
||||||
relations: ['userCredentials'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public attachSessionToResponse(response: Response, sessionId: string): void {
|
|
||||||
response.cookie('session_id', sessionId, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: true,
|
|
||||||
sameSite: 'strict',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async validateSessionUserAgent(
|
|
||||||
sessionId: string,
|
|
||||||
currentUserAgent: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
const session = await this.sessionRepository.findOne({
|
|
||||||
where: { sessionId: sessionId },
|
|
||||||
select: ['userAgent'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return session.userAgent === currentUserAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkSessionLimit(userId: string): Promise<void> {
|
|
||||||
const userSessions = await this.sessionRepository
|
|
||||||
.createQueryBuilder('session')
|
|
||||||
.leftJoinAndSelect('session.userCredentials', 'userCredentials')
|
|
||||||
.where('userCredentials.id = :userId', { userId })
|
|
||||||
.orderBy('session.expiresAt', 'ASC')
|
|
||||||
.getMany();
|
|
||||||
|
|
||||||
if (userSessions.length >= 5) {
|
|
||||||
await this.sessionRepository.delete(userSessions[0].id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async invalidateAllSessionsForUser(userId: string): Promise<void> {
|
|
||||||
await this.sessionRepository.delete({ userCredentials: userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async extendSessionExpiration(sessionId: string): Promise<void> {
|
|
||||||
const session = await this.sessionRepository.findOne({
|
|
||||||
where: { sessionId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (session) {
|
|
||||||
session.expiresAt = new Date(
|
|
||||||
session.expiresAt.setMinutes(session.expiresAt.getMinutes() + 30)
|
|
||||||
);
|
|
||||||
await this.sessionRepository.save(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Add cron job to clear expired sessions
|
|
||||||
public async clearExpiredSessions(): Promise<void> {
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
await this.sessionRepository.delete({ expiresAt: LessThan(now) });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,11 +31,11 @@ export class UserCredentialsRepository {
|
||||||
return this.repository.findOne({ where: { id: userId } });
|
return this.repository.findOne({ where: { id: userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateUserRefreshToken(
|
public async updateUserTokenHash(
|
||||||
userId: string,
|
userId: string,
|
||||||
refreshToken: string | null
|
hashedRt: string | null
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const result = await this.repository.update(userId, { refreshToken });
|
const result = await this.repository.update(userId, { hashedRt });
|
||||||
|
|
||||||
return result.affected ?? 0;
|
return result.affected ?? 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,12 @@
|
||||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||||
import { Response, Request } from 'express';
|
|
||||||
import { Session } from 'src/entities';
|
|
||||||
import { EncryptionService } from 'src/shared';
|
import { EncryptionService } from 'src/shared';
|
||||||
|
|
||||||
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
||||||
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
||||||
import { EmailVerificationService } from '../../verify-module/services/email-verification.service';
|
import { EmailVerificationService } from '../../verify-module/services/email-verification.service';
|
||||||
import {
|
import { TokensDto, UserCredentialsDto } from '../models/dto';
|
||||||
AccessTokenDto,
|
|
||||||
LoginResponseDto,
|
|
||||||
UserCredentialsDto,
|
|
||||||
} from '../models/dto';
|
|
||||||
import { TokenPayload } from '../models/types';
|
|
||||||
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
||||||
|
|
||||||
import { SessionService } from './session.service';
|
|
||||||
import { TokenManagementService } from './token-management.service';
|
import { TokenManagementService } from './token-management.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -24,13 +16,10 @@ export class AuthService {
|
||||||
private readonly userDataRepository: UserDataRepository,
|
private readonly userDataRepository: UserDataRepository,
|
||||||
private readonly tokenManagementService: TokenManagementService,
|
private readonly tokenManagementService: TokenManagementService,
|
||||||
private readonly passwordConfirmationMailService: PasswordConfirmationMailService,
|
private readonly passwordConfirmationMailService: PasswordConfirmationMailService,
|
||||||
private readonly emailVerificationService: EmailVerificationService,
|
private readonly emailVerificationService: EmailVerificationService
|
||||||
private readonly sessionService: SessionService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async signup(
|
public async signup(userCredentials: UserCredentialsDto): Promise<TokensDto> {
|
||||||
userCredentials: UserCredentialsDto
|
|
||||||
): Promise<LoginResponseDto> {
|
|
||||||
const passwordHashed = await EncryptionService.hashData(
|
const passwordHashed = await EncryptionService.hashData(
|
||||||
userCredentials.password
|
userCredentials.password
|
||||||
);
|
);
|
||||||
|
@ -55,11 +44,7 @@ export class AuthService {
|
||||||
return this.generateAndPersistTokens(user.id, user.email);
|
return this.generateAndPersistTokens(user.id, user.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async signin(
|
public async signin(userCredentials: UserCredentialsDto): Promise<TokensDto> {
|
||||||
userCredentials: UserCredentialsDto,
|
|
||||||
response: Response,
|
|
||||||
request: Request
|
|
||||||
): Promise<LoginResponseDto> {
|
|
||||||
const user = await this.userCredentialsRepository.findUserByEmail(
|
const user = await this.userCredentialsRepository.findUserByEmail(
|
||||||
userCredentials.email
|
userCredentials.email
|
||||||
);
|
);
|
||||||
|
@ -77,104 +62,56 @@ export class AuthService {
|
||||||
throw new ForbiddenException('Access Denied');
|
throw new ForbiddenException('Access Denied');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sessionService.checkSessionLimit(user.id);
|
return this.generateAndPersistTokens(user.id, user.email);
|
||||||
|
}
|
||||||
|
|
||||||
const sesseionId = await this.sessionService.createSession(
|
public async refresh(
|
||||||
user.id,
|
userId: string,
|
||||||
request.headers['user-agent']
|
refreshToken: string
|
||||||
|
): Promise<TokensDto> {
|
||||||
|
const user = await this.userCredentialsRepository.findUserById(userId);
|
||||||
|
|
||||||
|
if (!user || !user.hashedRt) {
|
||||||
|
throw new ForbiddenException('Access Denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTokenMatch = await EncryptionService.compareHash(
|
||||||
|
refreshToken,
|
||||||
|
user.hashedRt
|
||||||
);
|
);
|
||||||
|
|
||||||
this.sessionService.attachSessionToResponse(response, sesseionId.sessionId);
|
if (!refreshTokenMatch) {
|
||||||
|
throw new ForbiddenException('Access Denied');
|
||||||
|
}
|
||||||
|
|
||||||
return this.generateAndPersistTokens(user.id, user.email, true);
|
return this.generateAndPersistTokens(user.id, user.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout(userId: string): Promise<boolean> {
|
public async logout(userId: string): Promise<boolean> {
|
||||||
const affected =
|
const affected = await this.userCredentialsRepository.updateUserTokenHash(
|
||||||
await this.userCredentialsRepository.updateUserRefreshToken(userId, null);
|
userId,
|
||||||
|
null
|
||||||
await this.sessionService.invalidateAllSessionsForUser(userId);
|
);
|
||||||
|
|
||||||
return affected > 0;
|
return affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refresh(request: Request): Promise<AccessTokenDto> {
|
|
||||||
const sessionId = request.cookies['session_id'];
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
throw new ForbiddenException('Session ID missing');
|
|
||||||
}
|
|
||||||
|
|
||||||
const session: Session =
|
|
||||||
await this.sessionService.findSessionBySessionId(sessionId);
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
throw new ForbiddenException('Invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserAgentValid = await this.sessionService.validateSessionUserAgent(
|
|
||||||
sessionId,
|
|
||||||
request.headers['user-agent']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isUserAgentValid) {
|
|
||||||
throw new ForbiddenException('Invalid session - User agent mismatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sessionService.extendSessionExpiration(sessionId);
|
|
||||||
|
|
||||||
const decodedToken: TokenPayload = await this.validateRefreshToken(
|
|
||||||
session.userCredentials['id']
|
|
||||||
);
|
|
||||||
|
|
||||||
const newTokens = await this.generateAndPersistTokens(
|
|
||||||
decodedToken.sub,
|
|
||||||
decodedToken.email,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
return { access_token: newTokens.access_token };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async generateAndPersistTokens(
|
private async generateAndPersistTokens(
|
||||||
userId: string,
|
userId: string,
|
||||||
email: string,
|
email: string
|
||||||
updateRefreshToken: boolean = false
|
): Promise<TokensDto> {
|
||||||
): Promise<LoginResponseDto> {
|
|
||||||
const tokens = await this.tokenManagementService.generateTokens(
|
const tokens = await this.tokenManagementService.generateTokens(
|
||||||
userId,
|
userId,
|
||||||
email
|
email
|
||||||
);
|
);
|
||||||
|
const hashedRefreshToken = await EncryptionService.hashData(
|
||||||
if (updateRefreshToken) {
|
tokens.refresh_token
|
||||||
await this.userCredentialsRepository.updateUserRefreshToken(
|
|
||||||
userId,
|
|
||||||
tokens.refresh_token
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { access_token: tokens.access_token, email: email, userId: userId };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async validateRefreshToken(userId: string): Promise<TokenPayload> {
|
|
||||||
const user = await this.userCredentialsRepository.findUserById(userId);
|
|
||||||
|
|
||||||
if (!user || !user.refreshToken) {
|
|
||||||
throw new Error('No refresh token found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodedToken = await this.tokenManagementService.verifyRefreshToken(
|
|
||||||
user.refreshToken
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (decodedToken.exp < Date.now() / 1000) {
|
await this.userCredentialsRepository.updateUserTokenHash(
|
||||||
throw new Error('Token expired');
|
userId,
|
||||||
}
|
hashedRefreshToken
|
||||||
|
);
|
||||||
if (decodedToken.sub !== user.id) {
|
return tokens;
|
||||||
throw new Error('Token subject mismatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodedToken;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Response } from 'express';
|
|
||||||
import { Session } from 'src/entities';
|
|
||||||
|
|
||||||
import { SessionRepository } from '../repositories/session.repository';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SessionService {
|
|
||||||
public constructor(
|
|
||||||
@InjectRepository(Session)
|
|
||||||
private readonly sessionRepository: SessionRepository
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async createSession(
|
|
||||||
userId: string,
|
|
||||||
userAgent: string
|
|
||||||
): Promise<Session> {
|
|
||||||
return await this.sessionRepository.createSession(userId, userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async validateSessionUserAgent(
|
|
||||||
sessionId: string,
|
|
||||||
currentUserAgent: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
return await this.sessionRepository.validateSessionUserAgent(
|
|
||||||
sessionId,
|
|
||||||
currentUserAgent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkSessionLimit(userId: string): Promise<void> {
|
|
||||||
await this.sessionRepository.checkSessionLimit(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async invalidateAllSessionsForUser(userId: string): Promise<void> {
|
|
||||||
await this.sessionRepository.invalidateAllSessionsForUser(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async clearExpiredSessions(): Promise<void> {
|
|
||||||
await this.sessionRepository.clearExpiredSessions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async extendSessionExpiration(sessionId: string): Promise<void> {
|
|
||||||
await this.sessionRepository.extendSessionExpiration(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async findSessionBySessionId(sessionId: string): Promise<Session> {
|
|
||||||
return await this.sessionRepository.findSessionBySessionId(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public attachSessionToResponse(response: Response, sessionId: string): void {
|
|
||||||
this.sessionRepository.attachSessionToResponse(response, sessionId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
|
||||||
import { TokenPayload, Tokens } from '../models/types';
|
import { TokensDto } from '../models/dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TokenManagementService {
|
export class TokenManagementService {
|
||||||
|
@ -25,19 +25,16 @@ export class TokenManagementService {
|
||||||
this.JWT_SECRET_RT = this.configService.get<string>('JWT_SECRET_RT');
|
this.JWT_SECRET_RT = this.configService.get<string>('JWT_SECRET_RT');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async generateTokens(userId: string, email: string): Promise<Tokens> {
|
public async generateTokens(
|
||||||
|
userId: string,
|
||||||
|
email: string
|
||||||
|
): Promise<TokensDto> {
|
||||||
const access_token: string = await this.createAccessToken(userId, email);
|
const access_token: string = await this.createAccessToken(userId, email);
|
||||||
const refresh_token: string = await this.createRefreshToken(userId, email);
|
const refresh_token: string = await this.createRefreshToken(userId, email);
|
||||||
|
|
||||||
return { access_token, refresh_token };
|
return { access_token, refresh_token };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async verifyRefreshToken(token: string): Promise<TokenPayload> {
|
|
||||||
return this.jwt.verifyAsync(token, {
|
|
||||||
secret: this.JWT_SECRET_RT,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createAccessToken(
|
private async createAccessToken(
|
||||||
userId: string,
|
userId: string,
|
||||||
email: string
|
email: string
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
import {
|
import { EmailVerification, UserCredentials, UserData } from 'src/entities';
|
||||||
EmailVerification,
|
|
||||||
UserCredentials,
|
|
||||||
UserData,
|
|
||||||
Session,
|
|
||||||
} from 'src/entities';
|
|
||||||
|
|
||||||
export const databaseConfigFactory = (
|
export const databaseConfigFactory = (
|
||||||
configService: ConfigService
|
configService: ConfigService
|
||||||
|
@ -18,5 +13,5 @@ export const databaseConfigFactory = (
|
||||||
database: configService.get('DB_NAME'),
|
database: configService.get('DB_NAME'),
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: true,
|
logging: true,
|
||||||
entities: [UserCredentials, UserData, EmailVerification, Session],
|
entities: [UserCredentials, UserData, EmailVerification],
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,33 +1,15 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { RouterOutlet, Router } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
import { AuthService } from './shared/service';
|
import { AuthService } from './shared/service';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
providers: [],
|
providers: [AuthService],
|
||||||
imports: [RouterOutlet],
|
imports: [RouterOutlet],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent {
|
||||||
public constructor(
|
private readonly authService: AuthService = inject(AuthService);
|
||||||
private readonly authService: AuthService,
|
|
||||||
private readonly router: Router
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public ngOnInit(): void {
|
|
||||||
this.checkAuthentication();
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkAuthentication(): void {
|
|
||||||
this.authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => {
|
|
||||||
if (isAuthenticated) {
|
|
||||||
console.log('User is authenticated');
|
|
||||||
this.router.navigateByUrl('dashboard');
|
|
||||||
} else {
|
|
||||||
this.router.navigateByUrl('signup');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
import { AuthGuard } from './shared/guard/auth.guard';
|
export const routes: Routes = [
|
||||||
|
{ path: '', pathMatch: 'full', redirectTo: '' },
|
||||||
const publicRoutes: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
loadComponent: () => import('./app.component').then((m) => m.AppComponent),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'signup',
|
path: 'signup',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
|
@ -22,25 +17,3 @@ const publicRoutes: Routes = [
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const protectedRoutes: Routes = [
|
|
||||||
{
|
|
||||||
path: 'dashboard',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./pages/dashboard-root/dashboard-root.component').then(
|
|
||||||
(m) => m.DashboardRootComponent
|
|
||||||
),
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
children: [
|
|
||||||
...publicRoutes,
|
|
||||||
...protectedRoutes,
|
|
||||||
{ path: '', redirectTo: '', pathMatch: 'full' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
<h1>Hello World</h1>
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-dashboard-root',
|
|
||||||
standalone: true,
|
|
||||||
imports: [],
|
|
||||||
providers: [],
|
|
||||||
templateUrl: './dashboard-root.component.html',
|
|
||||||
styleUrl: './dashboard-root.component.scss',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
})
|
|
||||||
export class DashboardRootComponent {
|
|
||||||
public constructor() {}
|
|
||||||
}
|
|
|
@ -47,7 +47,7 @@ type AuthAction = 'register' | 'signup';
|
||||||
PasswordModule,
|
PasswordModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [AuthService],
|
||||||
templateUrl: './register-root.component.html',
|
templateUrl: './register-root.component.html',
|
||||||
styleUrl: './register-root.component.scss',
|
styleUrl: './register-root.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import {
|
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
CanActivateFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
UrlTree,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../service';
|
|
||||||
|
|
||||||
export const AuthGuard: CanActivateFn = (
|
|
||||||
route: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot
|
|
||||||
):
|
|
||||||
| Observable<boolean | UrlTree>
|
|
||||||
| Promise<boolean | UrlTree>
|
|
||||||
| boolean
|
|
||||||
| UrlTree => {
|
|
||||||
const authService: AuthService = inject(AuthService);
|
|
||||||
const router: Router = inject(Router);
|
|
||||||
|
|
||||||
authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => {
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
router.navigateByUrl('signup');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
|
@ -1,79 +1,51 @@
|
||||||
import {
|
import {
|
||||||
|
HttpErrorResponse,
|
||||||
|
HttpEvent,
|
||||||
|
HttpHandlerFn,
|
||||||
HttpInterceptorFn,
|
HttpInterceptorFn,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpHandlerFn,
|
|
||||||
HttpEvent,
|
|
||||||
HttpErrorResponse,
|
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
import { Observable, throwError } from 'rxjs';
|
import { Observable, catchError, switchMap, throwError } from 'rxjs';
|
||||||
import { catchError, switchMap } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { AuthService } from '../service';
|
import { AuthService } from '../service';
|
||||||
|
import { Tokens } from '../types';
|
||||||
|
|
||||||
export const AuthInterceptor: HttpInterceptorFn = (
|
export const AuthInterceptor: HttpInterceptorFn = (
|
||||||
request: HttpRequest<unknown>,
|
request: HttpRequest<unknown>,
|
||||||
next: HttpHandlerFn
|
next: HttpHandlerFn
|
||||||
): Observable<HttpEvent<unknown>> => {
|
): Observable<HttpEvent<unknown>> => {
|
||||||
const router = inject(Router);
|
const authService: AuthService = inject(AuthService);
|
||||||
const authService = inject(AuthService);
|
const accessToken: string | null = authService.access_token;
|
||||||
|
|
||||||
const handleRequest = (
|
if (accessToken) {
|
||||||
req: HttpRequest<unknown>
|
request = request.clone({
|
||||||
): Observable<HttpEvent<unknown>> => {
|
|
||||||
const accessToken = authService.access_token;
|
|
||||||
|
|
||||||
if (accessToken) {
|
|
||||||
req = addAuthHeader(req, accessToken);
|
|
||||||
}
|
|
||||||
return next(req);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAuthHeader = (
|
|
||||||
req: HttpRequest<unknown>,
|
|
||||||
token: string
|
|
||||||
): HttpRequest<unknown> => {
|
|
||||||
return req.clone({
|
|
||||||
setHeaders: {
|
setHeaders: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
return next(request).pipe(
|
||||||
|
catchError((error: HttpErrorResponse) => {
|
||||||
|
if (error.status === 401) {
|
||||||
|
return authService.refreshToken().pipe(
|
||||||
|
switchMap((tokens: Tokens) => {
|
||||||
|
request = request.clone({
|
||||||
|
setHeaders: {
|
||||||
|
Authorization: `Bearer ${tokens.access_token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return next(request);
|
||||||
|
}),
|
||||||
|
catchError((refreshError) => {
|
||||||
|
authService.signout();
|
||||||
|
return throwError(() => new Error(refreshError));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const handle401Error = (
|
return throwError(() => new Error());
|
||||||
req: HttpRequest<unknown>
|
})
|
||||||
): Observable<HttpEvent<unknown>> => {
|
|
||||||
console.log(authService.refresh_token);
|
|
||||||
if (!authService.refresh_token) {
|
|
||||||
router.navigateByUrl('signup');
|
|
||||||
return throwError(() => new Error('Authentication required'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return authService.refreshToken().pipe(
|
|
||||||
switchMap((tokens) => {
|
|
||||||
req = addAuthHeader(req, tokens.access_token);
|
|
||||||
return next(req);
|
|
||||||
}),
|
|
||||||
catchError((refreshError) => {
|
|
||||||
router.navigateByUrl('signup');
|
|
||||||
return throwError(() => new Error(refreshError));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleError = (
|
|
||||||
error: HttpErrorResponse,
|
|
||||||
req: HttpRequest<unknown>
|
|
||||||
): Observable<HttpEvent<unknown>> => {
|
|
||||||
if (error.status === 401) {
|
|
||||||
return handle401Error(req);
|
|
||||||
}
|
|
||||||
return throwError(() => new Error('Unhandled error'));
|
|
||||||
};
|
|
||||||
|
|
||||||
return handleRequest(request).pipe(
|
|
||||||
catchError((error) => handleError(error, request))
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, Observable, tap } from 'rxjs';
|
import { BehaviorSubject, Observable, tap } from 'rxjs';
|
||||||
|
@ -12,28 +13,22 @@ import { SessionStorageService } from './session-storage.service';
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
public isAuthenticated$: BehaviorSubject<boolean> =
|
|
||||||
new BehaviorSubject<boolean>(false);
|
|
||||||
private _access_token: string | null = null;
|
private _access_token: string | null = null;
|
||||||
private _refresh_token: string | null = null;
|
private _refresh_token: string | null = null;
|
||||||
|
private _isAuthenticated$: BehaviorSubject<boolean> =
|
||||||
|
new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
public get access_token(): string | null {
|
public get access_token(): string | null {
|
||||||
return this._access_token;
|
return this._access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get refresh_token(): string | null {
|
|
||||||
return this._refresh_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly httpClient: HttpClient,
|
||||||
private readonly localStorageService: LocalStorageService,
|
private readonly localStorageService: LocalStorageService,
|
||||||
private readonly sessionStorageService: SessionStorageService,
|
private readonly sessionStorageService: SessionStorageService,
|
||||||
private readonly authenticationApiService: AuthenticationApiService
|
private readonly authenticationApiService: AuthenticationApiService
|
||||||
) {
|
) {
|
||||||
this._access_token =
|
//this.autoLogin();
|
||||||
this.localStorageService.getItem<string>('access_token');
|
|
||||||
this._refresh_token =
|
|
||||||
this.sessionStorageService.getItem<string>('refresh_token');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public signin(credentials: LoginCredentials): void {
|
public signin(credentials: LoginCredentials): void {
|
||||||
|
@ -71,16 +66,31 @@ export class AuthService {
|
||||||
this._refresh_token = null;
|
this._refresh_token = null;
|
||||||
this.localStorageService.removeItem('access_token');
|
this.localStorageService.removeItem('access_token');
|
||||||
this.sessionStorageService.removeItem('refresh_token');
|
this.sessionStorageService.removeItem('refresh_token');
|
||||||
this.isAuthenticated$.next(false);
|
this._isAuthenticated$.next(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public autoLogin(): void {
|
||||||
|
const storedAccessToken: string | null =
|
||||||
|
this.localStorageService.getItem('access_token');
|
||||||
|
const storedRefreshToken: string | null =
|
||||||
|
this.sessionStorageService.getItem('refresh_token');
|
||||||
|
|
||||||
|
if (storedAccessToken && storedRefreshToken) {
|
||||||
|
this._refresh_token = storedRefreshToken;
|
||||||
|
this._isAuthenticated$.next(true);
|
||||||
|
//TODO Validate tokens with backend or decode JWT to check expiration
|
||||||
|
} else {
|
||||||
|
this.signout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleSuccess(tokens: Tokens): void {
|
private handleSuccess(tokens: Tokens): void {
|
||||||
this._access_token = tokens.access_token;
|
this._access_token = tokens.access_token;
|
||||||
this._refresh_token = tokens.refresh_token;
|
this._refresh_token = tokens.refresh_token;
|
||||||
this.localStorageService.setItem('access_token', tokens.access_token);
|
this.localStorageService.setItem('access_token', tokens.access_token);
|
||||||
this.sessionStorageService.setItem('refresh_token', tokens.refresh_token);
|
this.sessionStorageService.setItem('refresh_token', tokens.refresh_token);
|
||||||
this.isAuthenticated$.next(true);
|
this._isAuthenticated$.next(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue