diff --git a/package-lock.json b/package-lock.json index adcfcde..43a1390 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@simplewebauthn/server": "^9.0.3", "@supabase/supabase-js": "^2.49.4", "argon2": "^0.41.1", + "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "connect-redis": "^8.0.1", "express-session": "^1.18.1", @@ -5145,6 +5146,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.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", diff --git a/package.json b/package.json index a9bcdf2..4785635 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@simplewebauthn/server": "^9.0.3", "@supabase/supabase-js": "^2.49.4", "argon2": "^0.41.1", + "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "connect-redis": "^8.0.1", "express-session": "^1.18.1", diff --git a/src/auth/dto/challenge.dto.ts b/src/auth/dto/challenge.dto.ts index a2d6e95..6182478 100644 --- a/src/auth/dto/challenge.dto.ts +++ b/src/auth/dto/challenge.dto.ts @@ -1,7 +1,21 @@ import { Challenge } from '../entities/challenge.entity'; +import { ApiProperty } from '@nestjs/swagger'; export class ChallengeDTO { + @ApiProperty({ + description: 'The challenge string for WebAuthn registration', + example: 'dGhpcyBpcyBhIHNhbXBsZSBjaGFsbGVuZ2U=', + type: String, + required: true, + }) challenge: string; + + @ApiProperty({ + description: 'The expiration date of the challenge', + example: '2024-10-01T12:00:00Z', + type: Date, + required: true, + }) expiresAt: Date; constructor(challenge: Challenge) { diff --git a/src/auth/dto/register-user.input.ts b/src/auth/dto/register-user.input.ts index 2f6da87..669636f 100644 --- a/src/auth/dto/register-user.input.ts +++ b/src/auth/dto/register-user.input.ts @@ -1,5 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString, MinLength, IsNotEmpty } from 'class-validator'; + export class RegisterUserDto { + @ApiProperty({ + description: 'The username of the user', + example: 'john_doe', + }) + @IsNotEmpty() + @IsString() username: string; + + @ApiProperty({ + description: 'The email address of the user', + example: 'john.doe@email.com', + uniqueItems: true, + }) + @IsEmail() + @IsNotEmpty() + @IsString() email: string; + + @ApiProperty({ + description: 'The password for the user account', + example: 'S3cureP@ssw0rd!', + minLength: 8, + maxLength: 128, + }) + @IsNotEmpty() + @IsString() + @MinLength(8) password: string; } diff --git a/src/group/dto/create-group.dto.ts b/src/group/dto/create-group.dto.ts index 491ad55..d347588 100644 --- a/src/group/dto/create-group.dto.ts +++ b/src/group/dto/create-group.dto.ts @@ -1,10 +1,22 @@ import { IsNotEmpty, IsString, IsOptional, IsArray } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class CreateGroupDto { + @ApiProperty({ + description: 'The name of the group', + example: 'Fitness Enthusiasts', + maxLength: 100, + type: String, + }) @IsNotEmpty() @IsString() groupName: string; + @ApiProperty({ + description: 'The ids of the members to be added to the group', + example: ['user123', 'user456'], + type: [String], + }) @IsOptional() @IsArray() @IsString({ each: true }) diff --git a/src/main.ts b/src/main.ts index 30717c7..2149ce0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,9 +5,24 @@ import * as process from 'node:process'; import Redis from 'ioredis'; import { RedisStore } from 'connect-redis'; import * as passport from 'passport'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + // Global validation pipe + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + transformOptions: { + enableImplicitConversion: true, + }, + }), + ); + app.use( session({ secret: process.env.SESSION_SECRET as string, @@ -26,6 +41,27 @@ async function bootstrap() { app.use(passport.initialize()); app.use(passport.session()); + // Swagger API documentation setup + if (process.env.NODE_ENV !== 'production') { + const config = new DocumentBuilder() + .setTitle('MotiMate API') + .setDescription('API documentation for the MotiMate backend') + .setVersion('1.0') + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + in: 'header', + description: 'Enter your JWT token here', + }, + 'access-token', + ) + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + } + await app.listen(process.env.PORT ?? 3000); } diff --git a/src/user/dto/create-user.dto.ts b/src/user/dto/create-user.dto.ts index bc9d2f2..1c4d0a4 100644 --- a/src/user/dto/create-user.dto.ts +++ b/src/user/dto/create-user.dto.ts @@ -1,12 +1,42 @@ -import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; +import { IsEmail, IsNotEmpty, MinLength, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + export class CreateUserDto { + @ApiProperty({ + description: 'The name of the user', + example: 'John Doe', + required: true, + type: String, + minLength: 1, + maxLength: 255, + pattern: '^[a-zA-Z0-9 ]+$', // Allows alphanumeric characters and spaces + }) @IsNotEmpty() name: string; + @ApiProperty({ + description: 'The email of the user', + example: 'john.doe@email.com', + required: true, + type: String, + format: 'email', + pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', // Basic email validation + }) @IsEmail() + @IsString() + @IsNotEmpty() email: string; + @ApiProperty({ + description: 'The password of the user', + example: 'S3cureP@ssw0rd!', + required: true, + type: String, + minLength: 8, + maxLength: 128, + }) + @IsString() @IsNotEmpty() - @MinLength(6) //Confirm the minlength we are using + @MinLength(8) password: string; } diff --git a/src/user/dto/update-user.dto.ts b/src/user/dto/update-user.dto.ts index 7dac3d8..38e7999 100644 --- a/src/user/dto/update-user.dto.ts +++ b/src/user/dto/update-user.dto.ts @@ -1,13 +1,41 @@ -import { IsEmail, IsOptional, MinLength } from 'class-validator'; +import { IsEmail, IsOptional, MinLength, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + export class UpdateUserDto { + @ApiProperty({ + description: 'The name of the user', + example: 'John Doe', + required: false, + type: String, + minLength: 1, + maxLength: 255, + pattern: '^[a-zA-Z0-9 ]+$', // Allows alphanumeric characters and spaces + }) + @IsString() @IsOptional() name?: string; + @ApiProperty({ + description: 'The email of the user', + example: 'john.doe@email.com', + required: false, + type: String, + format: 'email', + pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', // Basic email validation + }) @IsOptional() @IsEmail() email?: string; + @ApiProperty({ + description: 'The password of the user', + example: 'S3cureP@ssw0rd!', + required: false, + type: String, + minLength: 8, + maxLength: 128, + }) @IsOptional() - @MinLength(6) + @MinLength(8) password?: string; } diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index 566e3d6..eb2dc40 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -15,6 +15,7 @@ import { UserWeeklyTarget } from '../../user-weekly-target/entities/user-weekly- import { Group } from '../../group/entities/group.entity'; import { PasskeyEntity } from '../../auth/entities/passkey.entity'; import { Challenge } from '../../auth/entities/challenge.entity'; +import { Exclude } from 'class-transformer'; @Entity() @Unique(['email']) @@ -32,7 +33,8 @@ export class User { @Column() account_status: boolean; - @Column({ length: 255, nullable: false }) + @Column({ length: 128, nullable: false }) + @Exclude({ toPlainOnly: true }) // Exclude password from serialization password: string; @Column({ length: 255 })