diff --git a/backend/database.postgres.sql b/backend/database.postgres.sql index b32d34d..5379805 100644 --- a/backend/database.postgres.sql +++ b/backend/database.postgres.sql @@ -99,7 +99,8 @@ CREATE TABLE user_id uuid NOT NULL, refresh_token_hash text NOT NULL, exp timestamp NOT NULL, - PRIMARY KEY (user_id, refresh_token_hash) + PRIMARY KEY (user_id, refresh_token_hash), + FOREIGN KEY (user_id) REFERENCES users (user_id) ); CREATE TABLE diff --git a/backend/nestjs-seshat-api/package-lock.json b/backend/nestjs-seshat-api/package-lock.json index 4453a35..21a2380 100644 --- a/backend/nestjs-seshat-api/package-lock.json +++ b/backend/nestjs-seshat-api/package-lock.json @@ -17,6 +17,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^11.0.0", "argon2": "^0.41.1", + "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", "moment": "^2.30.1", @@ -3530,6 +3531,12 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, "node_modules/class-validator": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", diff --git a/backend/nestjs-seshat-api/package.json b/backend/nestjs-seshat-api/package.json index da5edbd..47964db 100644 --- a/backend/nestjs-seshat-api/package.json +++ b/backend/nestjs-seshat-api/package.json @@ -28,6 +28,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^11.0.0", "argon2": "^0.41.1", + "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", "moment": "^2.30.1", diff --git a/backend/nestjs-seshat-api/src/auth/auth.access.service.ts b/backend/nestjs-seshat-api/src/auth/auth.access.service.ts index e1c2ff9..d20cd62 100644 --- a/backend/nestjs-seshat-api/src/auth/auth.access.service.ts +++ b/backend/nestjs-seshat-api/src/auth/auth.access.service.ts @@ -13,7 +13,7 @@ export class AuthAccessService { async generate(user: UserEntity) { const now = new Date(); - const limit = parseInt(this.config.getOrThrow('AUTH_JWT_ACCESS_TOKEN_EXPIRATION_MS')); + const limit = parseInt(this.config.getOrThrow('AUTH_JWT_ACCESS_TOKEN_EXPIRATION_MS')); const expiration = moment(now).add(limit, 'ms').toDate(); const token = await this.jwts.signAsync( @@ -25,7 +25,7 @@ export class AuthAccessService { exp: expiration.getTime(), }, { - secret: this.config.getOrThrow('AUTH_JWT_ACCESS_TOKEN_SECRET'), + secret: this.config.getOrThrow('AUTH_JWT_ACCESS_TOKEN_SECRET'), } ); diff --git a/backend/nestjs-seshat-api/src/auth/auth.controller.ts b/backend/nestjs-seshat-api/src/auth/auth.controller.ts index 8288449..afa3263 100644 --- a/backend/nestjs-seshat-api/src/auth/auth.controller.ts +++ b/backend/nestjs-seshat-api/src/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Request, Post, UseGuards, Get, Body, Res } from '@nestjs/common'; +import { Controller, Request, Post, UseGuards, Body, Res } from '@nestjs/common'; import { LoginAuthGuard } from './guards/login-auth.guard'; import { AuthService } from './auth.service'; import { UsersService } from 'src/users/users.service'; @@ -6,6 +6,8 @@ import { RegisterUserDto } from './dto/register-user.dto'; import { Response } from 'express'; import { JwtRefreshGuard } from './guards/jwt-refresh.guard'; import { OfflineGuard } from './guards/offline.guard'; +import { UserEntity } from 'src/users/users.entity'; +import { QueryFailedError } from 'typeorm'; @Controller('auth') export class AuthController { @@ -18,33 +20,68 @@ export class AuthController { @Res({ passthrough: true }) response: Response, ) { try { - const data = await this.auth.login(req.user); - + let data: AuthenticationDto | null; + try { + data = await this.auth.login(req.user); + if (!data.access_token || !data.refresh_token || !data.refresh_exp) { + return { + success: false, + error_message: 'Something went wrong with tokens while logging in.', + }; + } + } catch (err) { + if (err instanceof QueryFailedError) { + if (err.message.includes('duplicate key value violates unique constraint "users_user_login_key"')) { + return { + success: false, + error_message: 'Username already exist.', + }; + } + } + console.log('AuthController', typeof err, err); + return { + success: false, + error_message: 'Something went wrong while logging in.', + }; + } + response.cookie('Authentication', data.access_token, { httpOnly: true, secure: true, expires: new Date(data.exp), }); - + response.cookie('Refresh', data.refresh_token, { httpOnly: true, secure: true, expires: new Date(data.refresh_exp), }); - - return { success: true }; + + return { + success: true, + }; } catch (err) { console.log(err); return { success: false, error_message: 'Something went wrong.', - } + }; } } @UseGuards(LoginAuthGuard) @Post('logout') - async logout(@Request() req) { + async logout( + @Request() req, + @Res({ passthrough: true }) response: Response, + ) { + console.log('logout cookie', req.cookies?.Refresh); + // TODO: delete refresh token from database. + // await this.auth.delete(req.cookies?.Refresh); + + response.clearCookie('Refresh'); + response.clearCookie('Authentication'); + return req.logout(); } @@ -78,7 +115,7 @@ export class AuthController { return { success: false, error_message: 'Something went wrong.', - } + }; } } @@ -89,67 +126,57 @@ export class AuthController { @Res({ passthrough: true }) response: Response, @Body() body: RegisterUserDto, ) { + let user: UserEntity | null; + let data: AuthenticationDto | null; try { const { user_login, user_name, password } = body; - if (!user_login) { - return { success: false, error_message: 'No user login found.' }; + user = await this.users.register(user_login.toLowerCase(), user_name, password, true); + } catch (err) { + if (err instanceof QueryFailedError) { + if (err.message.includes('duplicate key value violates unique constraint "users_user_login_key"')) { + return { + success: false, + error_message: 'Username already exist.', + }; + } } - if (!user_name) { - return { success: false, error_message: 'No user name found.' }; - } - if (!password) { - return { success: false, error_message: 'No password found.' }; - } - if (user_name.length < 1) { - return { success: false, error_message: 'Name is too short.' }; - } - if (user_name.length > 32) { - return { success: false, error_message: 'Name is too long.' }; - } - if (user_login.length < 3) { - return { success: false, error_message: 'Login is too short.' }; - } - if (user_login.length > 12) { - return { success: false, error_message: 'Login is too long.' }; - } - if (password.length < 12) { - return { success: false, error_message: 'Password is too short.' }; - } - if (password.length > 64) { - return { success: false, error_message: 'Password is too long.' }; - } - - const user = await this.users.register(user_login.toLowerCase(), user_name, password, true); - if (!user) { - return { success: false, error_message: 'Failed to register' }; - } - - const data = await this.auth.login(user); - if (!data.access_token || !data.refresh_token || !data.refresh_exp) { - return { success: false, error_message: 'Something went wrong while logging in.' }; - } - - response.cookie('Authentication', data.access_token, { - httpOnly: true, - secure: true, - expires: new Date(data.exp), - }); - - response.cookie('Refresh', data.refresh_token, { - httpOnly: true, - secure: true, - expires: new Date(data.refresh_exp), - }); - + console.log('AuthController', err); return { - success: true, + success: false, + error_message: 'Something went wrong when creating user.', + }; + } + + try { + data = await this.auth.login(user); + if (!data.access_token || !data.refresh_token || !data.refresh_exp) { + return { + success: false, + error_message: 'Something went wrong with tokens while logging in.', + }; } } catch (err) { console.log('AuthController', err); return { success: false, - error_message: 'Something went wrong.', - } + error_message: 'Something went wrong while logging in.', + }; + } + + response.cookie('Authentication', data.access_token, { + httpOnly: true, + secure: true, + expires: new Date(data.exp), + }); + + response.cookie('Refresh', data.refresh_token, { + httpOnly: true, + secure: true, + expires: new Date(data.refresh_exp), + }); + + return { + success: true, } } } \ No newline at end of file diff --git a/backend/nestjs-seshat-api/src/auth/auth.refresh.service.ts b/backend/nestjs-seshat-api/src/auth/auth.refresh.service.ts index 7ee95a1..03ac12f 100644 --- a/backend/nestjs-seshat-api/src/auth/auth.refresh.service.ts +++ b/backend/nestjs-seshat-api/src/auth/auth.refresh.service.ts @@ -35,9 +35,9 @@ export class AuthRefreshService { // - token has reached expiration threshold; // - token has expired. const now = new Date(); - const threshhold = parseInt(this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_EXPIRATION_THRESHHOLD_MS')); + const threshhold = parseInt(this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_EXPIRATION_THRESHHOLD_MS')); if (!refreshToken || !expiration || now.getTime() - expiration.getTime() > threshhold) { - const limit = parseInt(this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_EXPIRATION_MS')); + const limit = parseInt(this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_EXPIRATION_MS')); expiration = moment(now).add(limit, 'ms').toDate(); refreshToken = await this.jwts.signAsync( { @@ -48,7 +48,7 @@ export class AuthRefreshService { exp: expiration.getTime(), }, { - secret: this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_SECRET'), + secret: this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_SECRET'), } ); diff --git a/backend/nestjs-seshat-api/src/auth/auth.service.ts b/backend/nestjs-seshat-api/src/auth/auth.service.ts index 5c5e8ba..2cacfcd 100644 --- a/backend/nestjs-seshat-api/src/auth/auth.service.ts +++ b/backend/nestjs-seshat-api/src/auth/auth.service.ts @@ -13,7 +13,7 @@ export class AuthService { ) { } - async login(user: UserEntity) { + async login(user: UserEntity): Promise { return this.renew(user, null); } @@ -27,7 +27,7 @@ export class AuthService { async renew( user: UserEntity, refresh_token: string - ): Promise<{ access_token: string, exp: number, refresh_token: string, refresh_exp: number }> { + ): Promise { const new_refresh_data = await this.refreshTokens.generate(user, refresh_token); const new_refresh_token = new_refresh_data.refresh_token; const new_refresh_exp = new_refresh_data.exp; diff --git a/backend/nestjs-seshat-api/src/auth/dto/authentication.dto.ts b/backend/nestjs-seshat-api/src/auth/dto/authentication.dto.ts new file mode 100644 index 0000000..2d54147 --- /dev/null +++ b/backend/nestjs-seshat-api/src/auth/dto/authentication.dto.ts @@ -0,0 +1,6 @@ +class AuthenticationDto { + access_token: string; + exp: number; + refresh_token: string | null; + refresh_exp: number | null; +} \ No newline at end of file diff --git a/backend/nestjs-seshat-api/src/auth/dto/register-user.dto.ts b/backend/nestjs-seshat-api/src/auth/dto/register-user.dto.ts index e9f6133..3f72aaa 100644 --- a/backend/nestjs-seshat-api/src/auth/dto/register-user.dto.ts +++ b/backend/nestjs-seshat-api/src/auth/dto/register-user.dto.ts @@ -1,12 +1,19 @@ -import { IsNotEmpty } from 'class-validator'; +import { IsAlphanumeric, IsNotEmpty, IsString, Length, MaxLength, MinLength } from 'class-validator'; export class RegisterUserDto { - @IsNotEmpty() + @IsString() + @Length(3, 16) + @IsAlphanumeric() readonly user_login: string; - @IsNotEmpty() + @IsString() + @Length(1, 32) readonly user_name: string; + @IsString() @IsNotEmpty() + @Length(12, 64) + @MinLength(12) + @MaxLength(64) readonly password: string; } \ No newline at end of file diff --git a/backend/nestjs-seshat-api/src/auth/jwt.options.ts b/backend/nestjs-seshat-api/src/auth/jwt.options.ts index 3a9822d..d8a6a95 100644 --- a/backend/nestjs-seshat-api/src/auth/jwt.options.ts +++ b/backend/nestjs-seshat-api/src/auth/jwt.options.ts @@ -12,8 +12,8 @@ export class JwtOptions implements JwtOptionsFactory { createJwtOptions(): Promise | JwtModuleOptions { return { signOptions: { - issuer: this.config.getOrThrow('AUTH_JWT_ISSUER'), - audience: this.config.getOrThrow('AUTH_JWT_AUDIENCE'), + issuer: this.config.getOrThrow('AUTH_JWT_ISSUER'), + audience: this.config.getOrThrow('AUTH_JWT_AUDIENCE'), }, }; } diff --git a/backend/nestjs-seshat-api/src/auth/strategies/jwt-refresh.strategy.ts b/backend/nestjs-seshat-api/src/auth/strategies/jwt-refresh.strategy.ts index 469801d..0255bf3 100644 --- a/backend/nestjs-seshat-api/src/auth/strategies/jwt-refresh.strategy.ts +++ b/backend/nestjs-seshat-api/src/auth/strategies/jwt-refresh.strategy.ts @@ -12,9 +12,9 @@ export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh' super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: config.get('AUTH_JWT_REFRESH_SECRET'), - issuer: config.getOrThrow('AUTH_JWT_ISSUER'), - audience: config.getOrThrow('AUTH_JWT_AUDIENCE'), + secretOrKey: config.get('AUTH_JWT_REFRESH_SECRET'), + issuer: config.getOrThrow('AUTH_JWT_ISSUER'), + audience: config.getOrThrow('AUTH_JWT_AUDIENCE'), passReqToCallback: true, }); } diff --git a/backend/nestjs-seshat-api/src/auth/strategies/jwt.strategy.ts b/backend/nestjs-seshat-api/src/auth/strategies/jwt.strategy.ts index 0998044..0a2cf93 100644 --- a/backend/nestjs-seshat-api/src/auth/strategies/jwt.strategy.ts +++ b/backend/nestjs-seshat-api/src/auth/strategies/jwt.strategy.ts @@ -11,9 +11,9 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: config.getOrThrow('AUTH_JWT_ACCESS_TOKEN_SECRET'), - issuer: config.getOrThrow('AUTH_JWT_ISSUER'), - audience: config.getOrThrow('AUTH_JWT_AUDIENCE'), + secretOrKey: config.getOrThrow('AUTH_JWT_ACCESS_TOKEN_SECRET'), + issuer: config.getOrThrow('AUTH_JWT_ISSUER'), + audience: config.getOrThrow('AUTH_JWT_AUDIENCE'), passReqToCallback: true, }); } diff --git a/backend/nestjs-seshat-api/src/database-config/database.options.ts b/backend/nestjs-seshat-api/src/database-config/database.options.ts index f5bb104..3d49c6c 100644 --- a/backend/nestjs-seshat-api/src/database-config/database.options.ts +++ b/backend/nestjs-seshat-api/src/database-config/database.options.ts @@ -12,11 +12,11 @@ export class DatabaseOptions implements TypeOrmOptionsFactory { createTypeOrmOptions(): TypeOrmModuleOptions | Promise { return { type: "postgres", - host: this.config.getOrThrow('DATABASE_HOST'), - port: parseInt(this.config.getOrThrow('DATABASE_PORT'), 10), - username: this.config.getOrThrow('DATABASE_USERNAME'), - password: this.config.getOrThrow('DATABASE_PASSWORD'), - database: this.config.getOrThrow('DATABASE_NAME'), + host: this.config.getOrThrow('DATABASE_HOST'), + port: parseInt(this.config.getOrThrow('DATABASE_PORT'), 10), + username: this.config.getOrThrow('DATABASE_USERNAME'), + password: this.config.getOrThrow('DATABASE_PASSWORD'), + database: this.config.getOrThrow('DATABASE_NAME'), entities: [__dirname + '/../**/*.entity.js'], logging: true, diff --git a/backend/nestjs-seshat-api/src/main.ts b/backend/nestjs-seshat-api/src/main.ts index 23efc15..1cd9128 100644 --- a/backend/nestjs-seshat-api/src/main.ts +++ b/backend/nestjs-seshat-api/src/main.ts @@ -1,10 +1,16 @@ import * as cookieParser from 'cookie-parser'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(cookieParser()); + app.useGlobalPipes(new ValidationPipe({ + stopAtFirstError: true, + whitelist: true, + transform: true, + })); await app.listen(process.env.PORT ?? 3001); } bootstrap();