diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 865fe76..0d93ef9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,17 +1,20 @@ ## Description + ## Type of Change + + - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Documentation update - [ ] Test addition/improvement ## Checklist + -- [ ] I have run `npm run lint` and fixed any errors + - [ ] I have run `npm run build` successfully with no errors - [ ] I have run `npm test` and all tests pass - [ ] If I added new test files, they all pass @@ -19,13 +22,16 @@ - [ ] I have performed a self-review of my code ## CI/CD Requirements + The following checks will run automatically on this PR: -1. ✅ **Lint Check** - Ensures code has no ESLint errors -2. ✅ **Build Check** - Ensures the project builds successfully -3. ✅ **Test Check** - Ensures all tests pass (including any new test files) + +1. ✅ **Build Check** - Ensures the project builds successfully +2. ✅ **Test Check** - Ensures all tests pass (including any new test files) ## Screenshots (if applicable) + -## Additional Notes - +## Closes + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f5c794..cc3c64d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,8 @@ on: branches: [main, master, develop] jobs: - lint-and-build: - name: Lint and Build Check + build: + name: Build Check runs-on: ubuntu-latest steps: @@ -24,16 +24,13 @@ jobs: - name: Install dependencies run: npm ci - - name: Run ESLint (Check for errors) - run: npm run lint - - name: Build project run: npm run build test: name: Test Check runs-on: ubuntu-latest - needs: lint-and-build + needs: build steps: - name: Checkout code diff --git a/package.json b/package.json index 0dfb155..03172f5 100644 --- a/package.json +++ b/package.json @@ -79,15 +79,18 @@ "json", "ts" ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", + "rootDir": ".", + "testRegex": "test/.*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "collectCoverageFrom": [ - "**/*.(t|j)s" + "src/**/*.(t|j)s" ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" + "coverageDirectory": "coverage", + "testEnvironment": "node", + "moduleNameMapper": { + "^src/(.*)$": "/src/$1" + } } } diff --git a/src/app.module.ts b/src/app.module.ts index f0b7f82..a7f2cce 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,10 +5,10 @@ import { ConfigModule } from '@nestjs/config'; import { DatabaseModule } from './database/database.module'; import { AuthModule } from './auth/auth.module'; import { UsersModule } from './users/users.module'; -import { MailModule } from './modules/mail/mail.module'; import configuration from './database/config'; import { LoggerModule } from './logger/logger.module'; import { ProjectsModule } from './projects/projects.module'; +import { MailModule } from './mail/mail.module'; @Module({ imports: [ diff --git a/src/auth/README.md b/src/auth/README.md deleted file mode 100644 index 88d8acb..0000000 --- a/src/auth/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# Authentication Module - -This module provides JWT-based authentication for the StellarAid API. - -## Features - -- User registration with email and wallet address -- User login with email/password -- JWT token generation and validation -- Protected routes using JWT authentication -- Password hashing with bcrypt - -## Endpoints - -### POST /auth/register -Register a new user - -**Request Body:** -```json -{ - "email": "user@example.com", - "password": "password123", - "firstName": "John", - "lastName": "Doe", - "walletAddress": "GABC123..." -} -``` - -**Response:** -```json -{ - "accessToken": "jwt-token-string", - "user": { - "id": "1", - "email": "user@example.com", - "firstName": "John", - "lastName": "Doe", - "walletAddress": "GABC123..." - } -} -``` - -### POST /auth/login -Login with existing credentials - -**Request Body:** -```json -{ - "email": "user@example.com", - "password": "password123" -} -``` - -**Response:** -```json -{ - "accessToken": "jwt-token-string", - "user": { - "id": "1", - "email": "user@example.com", - "firstName": "John", - "lastName": "Doe", - "walletAddress": "GABC123..." - } -} -``` - -### GET /auth/profile -Get current user profile (requires JWT token) - -**Headers:** -``` -Authorization: Bearer -``` - -**Response:** -```json -{ - "sub": "1", - "email": "user@example.com", - "walletAddress": "GABC123..." -} -``` - -## Environment Variables - -Add these to your `.env` file: - -```env -JWT_SECRET=your-super-secret-jwt-key-here-change-in-production -JWT_EXPIRES_IN=1d -``` - -## Dependencies - -- `@nestjs/jwt` - JWT token handling -- `@nestjs/passport` - Authentication middleware -- `bcrypt` - Password hashing -- `passport-jwt` - JWT strategy for Passport -- `class-validator` - Request validation - -## Security Notes - -- Passwords are hashed using bcrypt with 10 salt rounds -- JWT tokens are signed with a secret key -- All endpoints use validation decorators -- Current implementation uses in-memory storage (replace with database in production) - -## Testing - -Run the authentication tests: -```bash -npm run test auth -``` - -## Next Steps - -1. Replace in-memory storage with a proper database -2. Add user roles and permissions -3. Implement refresh tokens -4. Add email verification -5. Add rate limiting for auth endpoints -6. Implement proper error handling and logging \ No newline at end of file diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 83216c7..c4de6bd 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -20,13 +20,13 @@ import { ApiUnauthorizedResponse, ApiNotFoundResponse, } from '@nestjs/swagger'; -import { AuthService } from './auth.service'; -import { RegisterDto } from './dtos/register.dto'; -import { LoginDto } from './dtos/login.dto'; -import { VerifyEmailDto } from './dtos/verify-email.dto'; -import { ResendVerificationDto } from './dtos/resend-verification.dto'; -import { RefreshTokenDto } from './dtos/refresh-token.dto'; -import { AuthResponseDto } from './dtos/auth-response.dto'; +import { RegisterDto } from './dto/register.dto'; +import { LoginDto } from './dto/login.dto'; +import { VerifyEmailDto } from './dto/verify-email.dto'; +import { ResendVerificationDto } from './dto/resend-verification.dto'; +import { RefreshTokenDto } from './dto/refresh-token.dto'; +import { AuthResponseDto } from './dto/auth-response.dto'; +import { AuthService } from './providers/auth.service'; @ApiTags('auth') @ApiBearerAuth() @@ -34,6 +34,7 @@ import { AuthResponseDto } from './dtos/auth-response.dto'; export class AuthController { constructor(private readonly authService: AuthService) {} + //_____________________Endpoint to register a new user @Post('register') @ApiOperation({ summary: 'Register a new user' }) @ApiCreatedResponse({ @@ -47,6 +48,7 @@ export class AuthController { return this.authService.register(registerDto); } + //_________________________ Endpoint to login with email and password @Post('login') @ApiOperation({ summary: 'Login with email and password' }) @ApiOkResponse({ @@ -63,8 +65,9 @@ export class AuthController { return this.authService.login(loginDto, req); } - @UseGuards(AuthGuard('jwt')) + //_________________________ Endpoint to get current user profile (JWT required) @Get('profile') + @UseGuards(AuthGuard('jwt')) @ApiOperation({ summary: 'Get current user profile (JWT required)' }) @ApiOkResponse({ description: 'Profile retrieved successfully', @@ -77,6 +80,7 @@ export class AuthController { return req.user; } + //_________________________ Endpoint to verify email with token @Post('verify-email') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Verify email with token' }) @@ -92,6 +96,7 @@ export class AuthController { return this.authService.verifyEmail(verifyEmailDto); } + //_________________________ Endpoint to resend email verification @Post('resend-verification') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Resend email verification' }) @@ -107,6 +112,7 @@ export class AuthController { return this.authService.resendVerification(resendVerificationDto); } + //_________________________ Endpoint to refresh access token using refresh token (token rotation) @Post('refresh') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Refresh access token' }) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 899bbbc..9b3e1ca 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -5,7 +5,6 @@ import { RequestMethod, } from '@nestjs/common'; import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; import { PassportModule } from '@nestjs/passport'; import { JwtModule } from '@nestjs/jwt'; import { ConfigModule, ConfigService } from '@nestjs/config'; @@ -14,8 +13,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; import { JwtStrategy } from './strategies/jwt.strategy'; -import { TokenValidationMiddleware } from './middleware/token-validation.middleware'; -import { JwtAuthGuard } from './guard/jwt-auth.guard'; +import { TokenValidationMiddleware } from '../common/middleware/token-validation.middleware'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { AuthService } from './providers/auth.service'; @Module({ imports: [ diff --git a/src/auth/dtos/auth-response.dto.ts b/src/auth/dto/auth-response.dto.ts similarity index 95% rename from src/auth/dtos/auth-response.dto.ts rename to src/auth/dto/auth-response.dto.ts index 517feca..1177821 100644 --- a/src/auth/dtos/auth-response.dto.ts +++ b/src/auth/dto/auth-response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { UserRole } from '../../users/entities/user.entity'; +import { UserRole } from 'src/common/enums/user-role.enum'; export class UserDto { @ApiProperty({ diff --git a/src/auth/dtos/login.dto.ts b/src/auth/dto/login.dto.ts similarity index 100% rename from src/auth/dtos/login.dto.ts rename to src/auth/dto/login.dto.ts diff --git a/src/auth/dtos/refresh-token.dto.ts b/src/auth/dto/refresh-token.dto.ts similarity index 100% rename from src/auth/dtos/refresh-token.dto.ts rename to src/auth/dto/refresh-token.dto.ts diff --git a/src/auth/dtos/register.dto.ts b/src/auth/dto/register.dto.ts similarity index 96% rename from src/auth/dtos/register.dto.ts rename to src/auth/dto/register.dto.ts index d4b3160..269f1db 100644 --- a/src/auth/dtos/register.dto.ts +++ b/src/auth/dto/register.dto.ts @@ -8,7 +8,7 @@ import { Matches, } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { UserRole } from '../../users/entities/user.entity'; +import { UserRole } from 'src/common/enums/user-role.enum'; export class RegisterDto { @ApiProperty({ diff --git a/src/auth/dtos/resend-verification.dto.ts b/src/auth/dto/resend-verification.dto.ts similarity index 100% rename from src/auth/dtos/resend-verification.dto.ts rename to src/auth/dto/resend-verification.dto.ts diff --git a/src/auth/dtos/verify-email.dto.ts b/src/auth/dto/verify-email.dto.ts similarity index 100% rename from src/auth/dtos/verify-email.dto.ts rename to src/auth/dto/verify-email.dto.ts diff --git a/src/auth/auth.service.ts b/src/auth/providers/auth.service.ts similarity index 87% rename from src/auth/auth.service.ts rename to src/auth/providers/auth.service.ts index 52dda4c..cc36acb 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/providers/auth.service.ts @@ -14,17 +14,19 @@ import { Repository } from 'typeorm'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; import { ConfigService } from '@nestjs/config'; -import { RegisterDto } from './dtos/register.dto'; -import { LoginDto } from './dtos/login.dto'; -import { VerifyEmailDto } from './dtos/verify-email.dto'; -import { ResendVerificationDto } from './dtos/resend-verification.dto'; -import { RefreshTokenDto } from './dtos/refresh-token.dto'; -import { JwtPayload } from './interfaces/auth.interface'; -import { AuthResponseDto } from './dtos/auth-response.dto'; -import { User, UserRole, KYCStatus } from '../users/entities/user.entity'; -import { ChangePasswordDto } from '../users/dtos/change-password.dto'; -import { SubmitKYCDto } from '../users/dtos/submit-kyc.dto'; -import { UpdateKYCDto } from '../users/dtos/update-kyc.dto'; +import { User } from 'src/users/entities/user.entity'; +import { ChangePasswordDto } from 'src/users/dto/change-password.dto'; +import { RegisterDto } from '../dto/register.dto'; +import { AuthResponseDto } from '../dto/auth-response.dto'; +import { UserRole } from 'src/common/enums/user-role.enum'; +import { LoginDto } from '../dto/login.dto'; +import { JwtPayload } from 'src/common/interfaces/auth.interface'; +import { VerifyEmailDto } from '../dto/verify-email.dto'; +import { ResendVerificationDto } from '../dto/resend-verification.dto'; +import { SubmitKYCDto } from 'src/users/dto/submit-kyc.dto'; +import { KYCStatus } from 'src/common/enums/kyc-status.enum'; +import { UpdateKYCDto } from 'src/users/dto/update-kyc.dto'; +import { RefreshTokenDto } from '../dto/refresh-token.dto'; @Injectable() export class AuthService { @@ -38,7 +40,7 @@ export class AuthService { ) {} // Change password for a given user id - async changePassword( + public async changePassword( userId: string, changeDto: ChangePasswordDto, ): Promise<{ message: string }> { @@ -66,15 +68,11 @@ export class AuthService { // Try to send confirmation email if an email service is registered (optional) try { - // If an injected email service exposes a `sendPasswordChangedEmail` method, call it. - // We inject under token 'EMAIL_SERVICE' elsewhere in the app if available. - // @ts-ignore if ( (this as any).emailService && typeof (this as any).emailService.sendPasswordChangedEmail === 'function' ) { - // @ts-ignore await (this as any).emailService.sendPasswordChangedEmail( user.email, user.firstName, @@ -89,7 +87,8 @@ export class AuthService { return { message: 'Password changed successfully' }; } - async register(registerDto: RegisterDto): Promise { + // Register a new user + public async register(registerDto: RegisterDto): Promise { const existingUser = await this.userRepository.findOne({ where: { email: registerDto.email }, }); @@ -117,7 +116,11 @@ export class AuthService { return this.generateTokens(user); } - async login(loginDto: LoginDto, request?: any): Promise { + // Login user and return JWT tokens + public async login( + loginDto: LoginDto, + request?: any, + ): Promise { const user = await this.userRepository.findOne({ where: { email: loginDto.email }, }); @@ -141,7 +144,8 @@ export class AuthService { return this.generateTokens(user); } - async generateTokens(user: User): Promise { + // Generate access and refresh tokens for a user + public async generateTokens(user: User): Promise { const payload: JwtPayload = { sub: user.id, email: user.email, @@ -180,7 +184,8 @@ export class AuthService { }; } - async validateUser(payload: JwtPayload): Promise { + // Validate user from JWT payload (used by JwtStrategy) + public async validateUser(payload: JwtPayload): Promise { const user = await this.userRepository.findOne({ where: { id: payload.sub }, }); @@ -190,7 +195,8 @@ export class AuthService { return user; } - async verifyEmail( + // Verify email with token + public async verifyEmail( verifyEmailDto: VerifyEmailDto, ): Promise<{ message: string }> { // Basic implementation for now to fix controller errors @@ -217,7 +223,8 @@ export class AuthService { return { message: 'Email verified successfully' }; } - async resendVerification( + // Resend email verification + public async resendVerification( resendVerificationDto: ResendVerificationDto, ): Promise<{ message: string }> { const user = await this.userRepository.findOne({ @@ -236,7 +243,8 @@ export class AuthService { return { message: 'Verification email resent' }; } - async forgotPassword(email: string): Promise<{ message: string }> { + // Forgot password - send reset email with token + public async forgotPassword(email: string): Promise<{ message: string }> { const user = await this.userRepository.findOne({ where: { email } }); // Always return success for security reasons @@ -260,12 +268,10 @@ export class AuthService { const token = `${selector}.${validator}`; try { - // @ts-ignore if ( (this as any).emailService && typeof (this as any).emailService.sendPasswordResetEmail === 'function' ) { - // @ts-ignore await (this as any).emailService.sendPasswordResetEmail( user.email, token, @@ -284,7 +290,8 @@ export class AuthService { }; } - async resetPassword( + // Reset password using token + public async resetPassword( token: string, newPassword: string, ): Promise<{ message: string }> { @@ -348,7 +355,8 @@ export class AuthService { return { message: 'Password reset successfully' }; } - async submitKYC( + // Submit KYC documents for verification + public async submitKYC( userId: string, submitDto: SubmitKYCDto, ): Promise<{ message: string }> { @@ -382,7 +390,8 @@ export class AuthService { return { message: 'KYC documents submitted for review' }; } - async updateKYCStatus( + // Admin endpoint to update KYC status + public async updateKYCStatus( userId: string, updateDto: UpdateKYCDto, ): Promise<{ message: string }> { @@ -425,7 +434,8 @@ export class AuthService { return { message: `KYC status updated to ${updateDto.status}` }; } - async refreshToken( + // Refresh access token using refresh token (token rotation) + public async refreshToken( refreshTokenDto: RefreshTokenDto, ): Promise { try { diff --git a/src/auth/strategies/jwt.strategy.ts b/src/auth/strategies/jwt.strategy.ts index 9872dd9..6f62b8b 100644 --- a/src/auth/strategies/jwt.strategy.ts +++ b/src/auth/strategies/jwt.strategy.ts @@ -2,7 +2,7 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { JwtPayload } from '../interfaces/auth.interface'; +import { JwtPayload } from '../../common/interfaces/auth.interface'; export const JWT_STRATEGY = 'jwt'; diff --git a/src/auth/decorators/current-user.decorator.ts b/src/common/decorators/current-user.decorator.ts similarity index 63% rename from src/auth/decorators/current-user.decorator.ts rename to src/common/decorators/current-user.decorator.ts index 6aa85c5..3c779e9 100644 --- a/src/auth/decorators/current-user.decorator.ts +++ b/src/common/decorators/current-user.decorator.ts @@ -2,17 +2,6 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import type { Request } from 'express'; import type { JwtPayload } from '../interfaces/auth.interface'; -/** - * Extracts the authenticated user payload from the request. - * Returns the full JwtPayload or a specific field. - * - * @example - * @Get('profile') - * getProfile(@CurrentUser() user: JwtPayload) { ... } - * - * @Get('id') - * getId(@CurrentUser('sub') id: string) { ... } - */ export const CurrentUser = createParamDecorator( (field: keyof JwtPayload | undefined, ctx: ExecutionContext) => { const request = ctx diff --git a/src/auth/decorators/public.decorator.ts b/src/common/decorators/public.decorator.ts similarity index 50% rename from src/auth/decorators/public.decorator.ts rename to src/common/decorators/public.decorator.ts index 02f1850..b3845e1 100644 --- a/src/auth/decorators/public.decorator.ts +++ b/src/common/decorators/public.decorator.ts @@ -1,13 +1,4 @@ import { SetMetadata } from '@nestjs/common'; export const IS_PUBLIC_KEY = 'isPublic'; - -/** - * Marks a route or controller as publicly accessible — no JWT required. - * - * @example - * @Public() - * @Post('login') - * login() { ... } - */ export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/src/common/decorators/roles.decorator.ts b/src/common/decorators/roles.decorator.ts index 3288d7e..5f04631 100644 --- a/src/common/decorators/roles.decorator.ts +++ b/src/common/decorators/roles.decorator.ts @@ -1,5 +1,5 @@ import { SetMetadata } from '@nestjs/common'; -import { UserRole } from '../../users/entities/user.entity'; +import { UserRole } from '../enums/user-role.enum'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles); diff --git a/src/common/enums/kyc-status.enum.ts b/src/common/enums/kyc-status.enum.ts new file mode 100644 index 0000000..7500fe3 --- /dev/null +++ b/src/common/enums/kyc-status.enum.ts @@ -0,0 +1,6 @@ +export enum KYCStatus { + NONE = 'none', + PENDING = 'pending', + APPROVED = 'approved', + REJECTED = 'rejected', +} diff --git a/src/common/enums/project-category.enum.ts b/src/common/enums/project-category.enum.ts new file mode 100644 index 0000000..25e901f --- /dev/null +++ b/src/common/enums/project-category.enum.ts @@ -0,0 +1,10 @@ +export enum ProjectCategory { + HEALTH = 'health', + EDUCATION = 'education', + DISASTER_RELIEF = 'disaster_relief', + ENVIRONMENT = 'environment', + COMMUNITY = 'community', + TECHNOLOGY = 'technology', + ARTS = 'arts', + OTHER = 'other', +} diff --git a/src/common/enums/project-status.enum.ts b/src/common/enums/project-status.enum.ts new file mode 100644 index 0000000..4fccaa3 --- /dev/null +++ b/src/common/enums/project-status.enum.ts @@ -0,0 +1,8 @@ +export enum ProjectStatus { + DRAFT = 'draft', + PENDING = 'pending', + APPROVED = 'approved', + ACTIVE = 'active', + COMPLETED = 'completed', + REJECTED = 'rejected', +} diff --git a/src/common/enums/projects-sortBy.enum.ts b/src/common/enums/projects-sortBy.enum.ts new file mode 100644 index 0000000..ffe4ee0 --- /dev/null +++ b/src/common/enums/projects-sortBy.enum.ts @@ -0,0 +1,5 @@ +export enum ProjectSortBy { + NEWEST = 'newest', + MOST_FUNDED = 'most_funded', + ENDING_SOON = 'ending_soon', +} diff --git a/src/common/enums/user-role.enum.ts b/src/common/enums/user-role.enum.ts new file mode 100644 index 0000000..136d573 --- /dev/null +++ b/src/common/enums/user-role.enum.ts @@ -0,0 +1,6 @@ +export enum UserRole { + USER = 'user', + ADMIN = 'admin', + CREATOR = 'creator', + DONOR = 'donor', +} diff --git a/src/common/http-exeption.filter.ts b/src/common/filters/http-exeption.filter.ts similarity index 96% rename from src/common/http-exeption.filter.ts rename to src/common/filters/http-exeption.filter.ts index 3aceaed..959e21a 100644 --- a/src/common/http-exeption.filter.ts +++ b/src/common/filters/http-exeption.filter.ts @@ -7,7 +7,7 @@ import { Inject, } from '@nestjs/common'; import { Request, Response } from 'express'; -import { LoggerService } from '../logger/logger.service'; +import { LoggerService } from '../../logger/logger.service'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { diff --git a/src/auth/guard/jwt-auth.guard.ts b/src/common/guards/jwt-auth.guard.ts similarity index 91% rename from src/auth/guard/jwt-auth.guard.ts rename to src/common/guards/jwt-auth.guard.ts index 43040ea..b8b66ba 100644 --- a/src/auth/guard/jwt-auth.guard.ts +++ b/src/common/guards/jwt-auth.guard.ts @@ -3,7 +3,7 @@ import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import type { Observable } from 'rxjs'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; -import { JWT_STRATEGY } from '../strategies/jwt.strategy'; +import { JWT_STRATEGY } from '../../auth/strategies/jwt.strategy'; @Injectable() export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY) { diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts index ecd4083..dec1eb8 100644 --- a/src/common/guards/roles.guard.ts +++ b/src/common/guards/roles.guard.ts @@ -1,7 +1,7 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ROLES_KEY } from '../decorators/roles.decorator'; -import { UserRole } from '../../users/entities/user.entity'; +import { UserRole } from '../enums/user-role.enum'; @Injectable() export class RolesGuard implements CanActivate { diff --git a/src/auth/interfaces/auth.interface.ts b/src/common/interfaces/auth.interface.ts similarity index 85% rename from src/auth/interfaces/auth.interface.ts rename to src/common/interfaces/auth.interface.ts index 3fc324f..c91a6cb 100644 --- a/src/auth/interfaces/auth.interface.ts +++ b/src/common/interfaces/auth.interface.ts @@ -1,4 +1,4 @@ -import { UserRole } from '../../users/entities/user.entity'; +import { UserRole } from '../enums/user-role.enum'; export interface JwtPayload { sub: string; // user id diff --git a/src/auth/interfaces/email.interface.ts b/src/common/interfaces/email.interface.ts similarity index 100% rename from src/auth/interfaces/email.interface.ts rename to src/common/interfaces/email.interface.ts diff --git a/src/auth/middleware/token-validation.middleware.ts b/src/common/middleware/token-validation.middleware.ts similarity index 83% rename from src/auth/middleware/token-validation.middleware.ts rename to src/common/middleware/token-validation.middleware.ts index 4650ec7..d25e956 100644 --- a/src/auth/middleware/token-validation.middleware.ts +++ b/src/common/middleware/token-validation.middleware.ts @@ -5,10 +5,6 @@ import { } from '@nestjs/common'; import type { Request, Response, NextFunction } from 'express'; -/** - * Rejects structurally malformed Bearer tokens before they reach the guard. - * Signature and expiry validation remain with JwtStrategy via passport-jwt. - */ @Injectable() export class TokenValidationMiddleware implements NestMiddleware { use(req: Request, _res: Response, next: NextFunction): void { diff --git a/src/modules/mail/mail.module.ts b/src/mail/mail.module.ts similarity index 100% rename from src/modules/mail/mail.module.ts rename to src/mail/mail.module.ts diff --git a/src/modules/mail/mail.service.ts b/src/mail/mail.service.ts similarity index 100% rename from src/modules/mail/mail.service.ts rename to src/mail/mail.service.ts diff --git a/src/modules/mail/templates/login.ejs b/src/mail/templates/login.ejs similarity index 100% rename from src/modules/mail/templates/login.ejs rename to src/mail/templates/login.ejs diff --git a/src/modules/mail/templates/welcome.ejs b/src/mail/templates/welcome.ejs similarity index 100% rename from src/modules/mail/templates/welcome.ejs rename to src/mail/templates/welcome.ejs diff --git a/src/projects/dtos/create-project.dto.ts b/src/projects/dto/create-project.dto.ts similarity index 96% rename from src/projects/dtos/create-project.dto.ts rename to src/projects/dto/create-project.dto.ts index 7445bba..f787bb8 100644 --- a/src/projects/dtos/create-project.dto.ts +++ b/src/projects/dto/create-project.dto.ts @@ -9,7 +9,7 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ProjectCategory } from '../entities/project.entity'; +import { ProjectCategory } from 'src/common/enums/project-category.enum'; export class CreateProjectDto { @ApiProperty({ diff --git a/src/projects/dtos/get-projects-query.dto.ts b/src/projects/dto/get-projects-query.dto.ts similarity index 87% rename from src/projects/dtos/get-projects-query.dto.ts rename to src/projects/dto/get-projects-query.dto.ts index fe61aef..839175b 100644 --- a/src/projects/dtos/get-projects-query.dto.ts +++ b/src/projects/dto/get-projects-query.dto.ts @@ -1,13 +1,9 @@ import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiPropertyOptional } from '@nestjs/swagger'; -import { ProjectCategory, ProjectStatus } from '../entities/project.entity'; - -export enum ProjectSortBy { - NEWEST = 'newest', - MOST_FUNDED = 'most_funded', - ENDING_SOON = 'ending_soon', -} +import { ProjectCategory } from 'src/common/enums/project-category.enum'; +import { ProjectStatus } from 'src/common/enums/project-status.enum'; +import { ProjectSortBy } from 'src/common/enums/projects-sortBy.enum'; export class GetProjectsQueryDto { @ApiPropertyOptional({ diff --git a/src/projects/entities/project.entity.ts b/src/projects/entities/project.entity.ts index f5c5a0d..11837f4 100644 --- a/src/projects/entities/project.entity.ts +++ b/src/projects/entities/project.entity.ts @@ -11,26 +11,8 @@ import { } from 'typeorm'; import { User } from '../../users/entities/user.entity'; import { Donation } from './donation.entity'; - -export enum ProjectStatus { - DRAFT = 'draft', - PENDING = 'pending', - APPROVED = 'approved', - ACTIVE = 'active', - COMPLETED = 'completed', - REJECTED = 'rejected', -} - -export enum ProjectCategory { - HEALTH = 'health', - EDUCATION = 'education', - DISASTER_RELIEF = 'disaster_relief', - ENVIRONMENT = 'environment', - COMMUNITY = 'community', - TECHNOLOGY = 'technology', - ARTS = 'arts', - OTHER = 'other', -} +import { ProjectCategory } from 'src/common/enums/project-category.enum'; +import { ProjectStatus } from 'src/common/enums/project-status.enum'; @Entity('projects') @Index('IDX_projects_creator_id', ['creatorId']) diff --git a/src/projects/projects.controller.ts b/src/projects/projects.controller.ts index a5cdbcc..fbfd4d1 100644 --- a/src/projects/projects.controller.ts +++ b/src/projects/projects.controller.ts @@ -22,14 +22,14 @@ import { ApiNotFoundResponse, ApiForbiddenResponse, } from '@nestjs/swagger'; -import { ProjectsService } from './projects.service'; -import { GetProjectsQueryDto } from './dtos/get-projects-query.dto'; -import { CreateProjectDto } from './dtos/create-project.dto'; -import { Public } from '../auth/decorators/public.decorator'; -import { JwtAuthGuard } from '../auth/guard/jwt-auth.guard'; +import { GetProjectsQueryDto } from './dto/get-projects-query.dto'; +import { CreateProjectDto } from './dto/create-project.dto'; +import { Public } from '../common/decorators/public.decorator'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; import { RolesGuard } from '../common/guards/roles.guard'; import { Roles } from '../common/decorators/roles.decorator'; -import { UserRole } from '../users/entities/user.entity'; +import { UserRole } from 'src/common/enums/user-role.enum'; +import { ProjectsService } from './providers/projects.service'; @ApiTags('projects') @ApiBearerAuth() @@ -37,8 +37,10 @@ import { UserRole } from '../users/entities/user.entity'; export class ProjectsController { constructor(private readonly projectsService: ProjectsService) {} - @Public() + //______________________ Endpoint to create a new project (CREATOR role required) @Get() + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.CREATOR) @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get all projects with filtering and pagination' }) @ApiOkResponse({ description: 'Projects retrieved successfully' }) @@ -52,8 +54,9 @@ export class ProjectsController { }; } - @Public() + //_____________________ Endpoint to get detailed project info by ID (public view) @Get(':id') + @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get detailed project by ID' }) @ApiOkResponse({ description: 'Project details retrieved successfully' }) @@ -62,6 +65,7 @@ export class ProjectsController { return this.projectsService.findOnePublic(id); } + //_____________________ Endpoint to create a new project (CREATOR role required) @Post() @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.CREATOR) diff --git a/src/projects/projects.module.ts b/src/projects/projects.module.ts index 07a3dcc..cb558ec 100644 --- a/src/projects/projects.module.ts +++ b/src/projects/projects.module.ts @@ -2,8 +2,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Project } from './entities/project.entity'; import { Donation } from './entities/donation.entity'; -import { ProjectsService } from './projects.service'; import { ProjectsController } from './projects.controller'; +import { ProjectsService } from './providers/projects.service'; @Module({ imports: [TypeOrmModule.forFeature([Project, Donation])], diff --git a/src/projects/projects.service.ts b/src/projects/providers/projects.service.ts similarity index 91% rename from src/projects/projects.service.ts rename to src/projects/providers/projects.service.ts index 84933f9..e7c82ef 100644 --- a/src/projects/projects.service.ts +++ b/src/projects/providers/projects.service.ts @@ -5,13 +5,12 @@ import { } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, SelectQueryBuilder } from 'typeorm'; -import { Project, ProjectStatus } from './entities/project.entity'; -import { Donation } from './entities/donation.entity'; -import { - GetProjectsQueryDto, - ProjectSortBy, -} from './dtos/get-projects-query.dto'; -import { CreateProjectDto } from './dtos/create-project.dto'; +import { ProjectStatus } from 'src/common/enums/project-status.enum'; +import { ProjectSortBy } from 'src/common/enums/projects-sortBy.enum'; +import { Project } from '../entities/project.entity'; +import { Donation } from '../entities/donation.entity'; +import { CreateProjectDto } from '../dto/create-project.dto'; +import { GetProjectsQueryDto } from '../dto/get-projects-query.dto'; @Injectable() export class ProjectsService { @@ -22,7 +21,8 @@ export class ProjectsService { private readonly donationRepository: Repository, ) {} - async create( + // create a new project + public async create( createProjectDto: CreateProjectDto, creatorId: string, ): Promise { @@ -61,7 +61,8 @@ export class ProjectsService { return this.projectRepository.save(project); } - async findAll( + // get all projects with filtering, sorting, and pagination + public async findAll( query: GetProjectsQueryDto, ): Promise<{ data: Partial[]; total: number }> { const { @@ -135,7 +136,8 @@ export class ProjectsService { return { data, total }; } - async findOnePublic(id: string) { + // get detailed project info by ID (public view) + public async findOnePublic(id: string) { const project = await this.projectRepository .createQueryBuilder('project') .leftJoinAndSelect('project.creator', 'creator') diff --git a/src/users/admin-users.controller.ts b/src/users/admin-users.controller.ts index 464caa7..4783791 100644 --- a/src/users/admin-users.controller.ts +++ b/src/users/admin-users.controller.ts @@ -19,10 +19,10 @@ import { } from '@nestjs/swagger'; import { Roles } from '../common/decorators/roles.decorator'; import { RolesGuard } from '../common/guards/roles.guard'; -import { UserRole } from './entities/user.entity'; -import { UsersService } from './users.service'; -import { AdminGetUsersQueryDto } from './dtos/admin-get-users-query.dto'; -import { UpdateUserRoleDto } from './dtos/update-user-role.dto'; +import { AdminGetUsersQueryDto } from './dto/admin-get-users-query.dto'; +import { UpdateUserRoleDto } from './dto/update-user-role.dto'; +import { UserRole } from 'src/common/enums/user-role.enum'; +import { UsersService } from './providers/users.service'; @ApiTags('Users') @ApiBearerAuth('JWT-auth') diff --git a/src/users/dtos/admin-get-users-query.dto.ts b/src/users/dto/admin-get-users-query.dto.ts similarity index 91% rename from src/users/dtos/admin-get-users-query.dto.ts rename to src/users/dto/admin-get-users-query.dto.ts index 068b9e0..a33688d 100644 --- a/src/users/dtos/admin-get-users-query.dto.ts +++ b/src/users/dto/admin-get-users-query.dto.ts @@ -1,7 +1,8 @@ import { Type } from 'class-transformer'; import { IsEnum, IsInt, IsOptional, Matches, Max, Min } from 'class-validator'; import { ApiPropertyOptional } from '@nestjs/swagger'; -import { KYCStatus, UserRole } from '../entities/user.entity'; +import { UserRole } from 'src/common/enums/user-role.enum'; +import { KYCStatus } from 'src/common/enums/kyc-status.enum'; export class AdminGetUsersQueryDto { @ApiPropertyOptional({ diff --git a/src/users/dtos/change-password.dto.ts b/src/users/dto/change-password.dto.ts similarity index 100% rename from src/users/dtos/change-password.dto.ts rename to src/users/dto/change-password.dto.ts diff --git a/src/users/dtos/forgot-password.dto.ts b/src/users/dto/forgot-password.dto.ts similarity index 100% rename from src/users/dtos/forgot-password.dto.ts rename to src/users/dto/forgot-password.dto.ts diff --git a/src/users/dtos/profile-response.dto.ts b/src/users/dto/profile-response.dto.ts similarity index 94% rename from src/users/dtos/profile-response.dto.ts rename to src/users/dto/profile-response.dto.ts index f23757b..015a490 100644 --- a/src/users/dtos/profile-response.dto.ts +++ b/src/users/dto/profile-response.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { UserRole, KYCStatus } from '../entities/user.entity'; +import { KYCStatus } from 'src/common/enums/kyc-status.enum'; +import { UserRole } from 'src/common/enums/user-role.enum'; export class ProfileResponseDto { @ApiProperty({ diff --git a/src/users/dtos/reset-password.dto.ts b/src/users/dto/reset-password.dto.ts similarity index 100% rename from src/users/dtos/reset-password.dto.ts rename to src/users/dto/reset-password.dto.ts diff --git a/src/users/dtos/submit-kyc.dto.ts b/src/users/dto/submit-kyc.dto.ts similarity index 100% rename from src/users/dtos/submit-kyc.dto.ts rename to src/users/dto/submit-kyc.dto.ts diff --git a/src/users/dtos/update-kyc.dto.ts b/src/users/dto/update-kyc.dto.ts similarity index 89% rename from src/users/dtos/update-kyc.dto.ts rename to src/users/dto/update-kyc.dto.ts index afa2f94..bbcb329 100644 --- a/src/users/dtos/update-kyc.dto.ts +++ b/src/users/dto/update-kyc.dto.ts @@ -1,6 +1,6 @@ import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { KYCStatus } from '../entities/user.entity'; +import { KYCStatus } from 'src/common/enums/kyc-status.enum'; export class UpdateKYCDto { @ApiProperty({ diff --git a/src/users/dtos/update-user-role.dto.ts b/src/users/dto/update-user-role.dto.ts similarity index 82% rename from src/users/dtos/update-user-role.dto.ts rename to src/users/dto/update-user-role.dto.ts index 2b622f6..2e641aa 100644 --- a/src/users/dtos/update-user-role.dto.ts +++ b/src/users/dto/update-user-role.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum } from 'class-validator'; -import { UserRole } from '../entities/user.entity'; +import { UserRole } from 'src/common/enums/user-role.enum'; export class UpdateUserRoleDto { @ApiProperty({ diff --git a/src/users/dtos/update-user.dto.ts b/src/users/dto/update-user.dto.ts similarity index 100% rename from src/users/dtos/update-user.dto.ts rename to src/users/dto/update-user.dto.ts diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index 03b0df3..7c34772 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -1,3 +1,5 @@ +import { KYCStatus } from 'src/common/enums/kyc-status.enum'; +import { UserRole } from 'src/common/enums/user-role.enum'; import { Entity, PrimaryGeneratedColumn, @@ -8,20 +10,6 @@ import { DeleteDateColumn, } from 'typeorm'; -export enum UserRole { - USER = 'user', - ADMIN = 'admin', - CREATOR = 'creator', - DONOR = 'donor', -} - -export enum KYCStatus { - NONE = 'none', - PENDING = 'pending', - APPROVED = 'approved', - REJECTED = 'rejected', -} - @Entity('users') @Index('IDX_users_email', ['email']) @Index('IDX_users_wallet_address', ['walletAddress']) diff --git a/src/users/users.service.ts b/src/users/providers/users.service.ts similarity index 79% rename from src/users/users.service.ts rename to src/users/providers/users.service.ts index 5e63156..a6661d9 100644 --- a/src/users/users.service.ts +++ b/src/users/providers/users.service.ts @@ -5,10 +5,12 @@ import { } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { User, KYCStatus, UserRole } from './entities/user.entity'; -import { ProfileResponseDto } from './dtos/profile-response.dto'; -import { UpdateUserDto } from './dtos/update-user.dto'; -import { AdminGetUsersQueryDto } from './dtos/admin-get-users-query.dto'; +import { User } from '../entities/user.entity'; +import { ProfileResponseDto } from '../dto/profile-response.dto'; +import { UpdateUserDto } from '../dto/update-user.dto'; +import { KYCStatus } from 'src/common/enums/kyc-status.enum'; +import { AdminGetUsersQueryDto } from '../dto/admin-get-users-query.dto'; +import { UserRole } from 'src/common/enums/user-role.enum'; @Injectable() export class UsersService { @@ -17,7 +19,29 @@ export class UsersService { private readonly userRepository: Repository, ) {} - async findById(id: string): Promise { + private toAdminUserResponse(user: User) { + return { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + role: user.role, + walletAddress: user.walletAddress, + country: user.country, + bio: user.bio, + avatarUrl: user.avatarUrl, + isEmailVerified: user.isEmailVerified, + kycStatus: user.kycStatus, + kycSubmittedAt: user.kycSubmittedAt, + kycVerifiedAt: user.kycVerifiedAt, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + deletedAt: user.deletedAt, + }; + } + + // find user by ID or throw NotFoundException + public async findById(id: string): Promise { const user = await this.userRepository.findOne({ where: { id } }); if (!user) { throw new NotFoundException('User not found'); @@ -25,11 +49,13 @@ export class UsersService { return user; } - async findByEmail(email: string): Promise { + // find user by email + public async findByEmail(email: string): Promise { return this.userRepository.findOne({ where: { email } }); } - async updateWalletAddress( + // find user by wallet address + public async updateWalletAddress( userId: string, walletAddress: string, ): Promise { @@ -38,7 +64,8 @@ export class UsersService { return this.userRepository.save(user); } - async updateProfile( + // update user profile + public async updateProfile( userId: string, dto: UpdateUserDto, ): Promise { @@ -65,7 +92,8 @@ export class UsersService { return this.getProfile(userId); } - async getProfile(userId: string): Promise { + // get user profile with profile completion percentage + public async getProfile(userId: string): Promise { const user = await this.findById(userId); // 6 checkpoints: email, firstName, lastName (always present), @@ -103,7 +131,8 @@ export class UsersService { }; } - async findAllForAdmin(query: AdminGetUsersQueryDto) { + // Find all users with optional filtering for admin panel + public async findAllForAdmin(query: AdminGetUsersQueryDto) { const qb = this.userRepository .createQueryBuilder('user') .where('user.deletedAt IS NULL'); @@ -141,42 +170,24 @@ export class UsersService { }; } - async getUserByIdForAdmin(id: string) { + // get user by ID for admin view (includes sensitive info) + public async getUserByIdForAdmin(id: string) { const user = await this.findById(id); return this.toAdminUserResponse(user); } - async updateUserRole(id: string, role: UserRole) { + // update user role (admin only) + public async updateUserRole(id: string, role: UserRole) { const user = await this.findById(id); user.role = role; const updatedUser = await this.userRepository.save(user); return this.toAdminUserResponse(updatedUser); } - async softDeleteUser(id: string) { + // soft delete user (admin only) + public async softDeleteUser(id: string) { const user = await this.findById(id); await this.userRepository.softDelete(user.id); return { message: 'User deleted successfully' }; } - - private toAdminUserResponse(user: User) { - return { - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - role: user.role, - walletAddress: user.walletAddress, - country: user.country, - bio: user.bio, - avatarUrl: user.avatarUrl, - isEmailVerified: user.isEmailVerified, - kycStatus: user.kycStatus, - kycSubmittedAt: user.kycSubmittedAt, - kycVerifiedAt: user.kycVerifiedAt, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - deletedAt: user.deletedAt, - }; - } } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index db02a80..c584cbd 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -19,18 +19,18 @@ import { ApiBearerAuth, ApiParam, } from '@nestjs/swagger'; -import { AuthService } from '../auth/auth.service'; -import { UsersService } from './users.service'; -import { ChangePasswordDto } from './dtos/change-password.dto'; -import { ForgotPasswordDto } from './dtos/forgot-password.dto'; -import { ResetPasswordDto } from './dtos/reset-password.dto'; -import { SubmitKYCDto } from './dtos/submit-kyc.dto'; -import { UpdateKYCDto } from './dtos/update-kyc.dto'; -import { UpdateUserDto } from './dtos/update-user.dto'; -import { Public } from '../auth/decorators/public.decorator'; -import { CurrentUser } from '../auth/decorators/current-user.decorator'; -import { UserRole } from './entities/user.entity'; -import type { JwtPayload } from '../auth/interfaces/auth.interface'; +import { ChangePasswordDto } from './dto/change-password.dto'; +import { ForgotPasswordDto } from './dto/forgot-password.dto'; +import { ResetPasswordDto } from './dto/reset-password.dto'; +import { SubmitKYCDto } from './dto/submit-kyc.dto'; +import { UpdateKYCDto } from './dto/update-kyc.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { Public } from '../common/decorators/public.decorator'; +import type { JwtPayload } from '../common/interfaces/auth.interface'; +import { CurrentUser } from 'src/common/decorators/current-user.decorator'; +import { UserRole } from 'src/common/enums/user-role.enum'; +import { AuthService } from 'src/auth/providers/auth.service'; +import { UsersService } from './providers/users.service'; @ApiTags('Users') @Controller('users') diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 75d1f4a..864233c 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; -import { UsersService } from './users.service'; import { AuthModule } from '../auth/auth.module'; import { User } from './entities/user.entity'; import { AdminUsersController } from './admin-users.controller'; import { RolesGuard } from '../common/guards/roles.guard'; +import { UsersService } from './providers/users.service'; @Module({ imports: [AuthModule, TypeOrmModule.forFeature([User])], diff --git a/src/app.controller.spec.ts b/test/app.controller.spec.ts similarity index 84% rename from src/app.controller.spec.ts rename to test/app.controller.spec.ts index d22f389..a4c0cd1 100644 --- a/src/app.controller.spec.ts +++ b/test/app.controller.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { AppController } from 'src/app.controller'; +import { AppService } from 'src/app.service'; describe('AppController', () => { let appController: AppController; diff --git a/src/auth/auth.controller.spec.ts b/test/auth/auth.controller.spec.ts similarity index 92% rename from src/auth/auth.controller.spec.ts rename to test/auth/auth.controller.spec.ts index 9bd07c6..559adc9 100644 --- a/src/auth/auth.controller.spec.ts +++ b/test/auth/auth.controller.spec.ts @@ -1,7 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; -import { UserRole } from '../users/entities/user.entity'; +import { AuthController } from 'src/auth/auth.controller'; +import { AuthService } from 'src/auth/providers/auth.service'; +import { UserRole } from 'src/common/enums/user-role.enum'; + describe('AuthController', () => { let authController: AuthController; diff --git a/src/auth/guard/jwt-auth.guard.spec.ts b/test/guards/jwt-auth.guard.spec.ts similarity index 91% rename from src/auth/guard/jwt-auth.guard.spec.ts rename to test/guards/jwt-auth.guard.spec.ts index 15dc657..7d78902 100644 --- a/src/auth/guard/jwt-auth.guard.spec.ts +++ b/test/guards/jwt-auth.guard.spec.ts @@ -2,13 +2,16 @@ import { ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Test } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; -import { JwtAuthGuard } from './jwt-auth.guard'; -import { JwtStrategy } from '../strategies/jwt.strategy'; -import { TokenValidationMiddleware } from '../middleware/token-validation.middleware'; -import { JwtPayload } from '../interfaces/auth.interface'; +import { JwtAuthGuard } from '../../src/common/guards/jwt-auth.guard'; +import { JwtStrategy } from '../../src/auth/strategies/jwt.strategy'; +import { TokenValidationMiddleware } from '../../src/common/middleware/token-validation.middleware'; +import { JwtPayload } from '../../src/common/interfaces/auth.interface'; import type { Request, Response } from 'express'; -import { IS_PUBLIC_KEY, Public } from '../decorators/public.decorator'; -import { UserRole } from '../../users/entities/user.entity'; +import { + IS_PUBLIC_KEY, + Public, +} from '../../src/common/decorators/public.decorator'; +import { UserRole } from '../../src/common/enums/user-role.enum'; function mockContext(isPublic = false): ExecutionContext { const handler = isPublic diff --git a/src/common/guards/roles.guard.spec.ts b/test/guards/roles.guard.spec.ts similarity index 94% rename from src/common/guards/roles.guard.spec.ts rename to test/guards/roles.guard.spec.ts index 29da600..9679f80 100644 --- a/src/common/guards/roles.guard.spec.ts +++ b/test/guards/roles.guard.spec.ts @@ -1,12 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Reflector } from '@nestjs/core'; -import { RolesGuard } from './roles.guard'; -import { UserRole } from '../../users/entities/user.entity'; -import { - ExecutionContext, - ForbiddenException, - UnauthorizedException, -} from '@nestjs/common'; + +import { ExecutionContext } from '@nestjs/common'; +import { RolesGuard } from 'src/common/guards/roles.guard'; +import { UserRole } from 'src/common/enums/user-role.enum'; describe('RolesGuard', () => { let guard: RolesGuard; diff --git a/src/modules/mail/mail.service.spec.ts b/test/mail/mail.service.spec.ts similarity index 99% rename from src/modules/mail/mail.service.spec.ts rename to test/mail/mail.service.spec.ts index 384e1a7..2e113b9 100644 --- a/src/modules/mail/mail.service.spec.ts +++ b/test/mail/mail.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; -import { MailService } from './mail.service'; import * as nodemailer from 'nodemailer'; +import { MailService } from 'src/mail/mail.service'; jest.mock('nodemailer'); jest.mock('ejs'); diff --git a/src/projects/projects.controller.spec.ts b/test/projects/projects.controller.spec.ts similarity index 96% rename from src/projects/projects.controller.spec.ts rename to test/projects/projects.controller.spec.ts index 322a31b..a219626 100644 --- a/src/projects/projects.controller.spec.ts +++ b/test/projects/projects.controller.spec.ts @@ -1,15 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ProjectsController } from './projects.controller'; -import { ProjectsService } from './projects.service'; -import { - GetProjectsQueryDto, - ProjectSortBy, -} from './dtos/get-projects-query.dto'; -import { - Project, - ProjectStatus, - ProjectCategory, -} from './entities/project.entity'; +import { ProjectCategory } from 'src/common/enums/project-category.enum'; +import { ProjectStatus } from 'src/common/enums/project-status.enum'; +import { ProjectSortBy } from 'src/common/enums/projects-sortBy.enum'; +import { GetProjectsQueryDto } from 'src/projects/dto/get-projects-query.dto'; +import { Project } from 'src/projects/entities/project.entity'; +import { ProjectsController } from 'src/projects/projects.controller'; +import { ProjectsService } from 'src/projects/providers/projects.service'; + describe('ProjectsController', () => { let controller: ProjectsController; diff --git a/src/projects/projects.service.spec.ts b/test/projects/projects.service.spec.ts similarity index 96% rename from src/projects/projects.service.spec.ts rename to test/projects/projects.service.spec.ts index 37411af..9e52606 100644 --- a/src/projects/projects.service.spec.ts +++ b/test/projects/projects.service.spec.ts @@ -1,17 +1,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { ProjectCategory } from 'src/common/enums/project-category.enum'; +import { ProjectStatus } from 'src/common/enums/project-status.enum'; +import { ProjectSortBy } from 'src/common/enums/projects-sortBy.enum'; +import { GetProjectsQueryDto } from 'src/projects/dto/get-projects-query.dto'; +import { Donation } from 'src/projects/entities/donation.entity'; +import { Project } from 'src/projects/entities/project.entity'; +import { ProjectsService } from 'src/projects/providers/projects.service'; import { Repository } from 'typeorm'; -import { ProjectsService } from './projects.service'; -import { - Project, - ProjectStatus, - ProjectCategory, -} from './entities/project.entity'; -import { Donation } from './entities/donation.entity'; -import { - GetProjectsQueryDto, - ProjectSortBy, -} from './dtos/get-projects-query.dto'; + describe('ProjectsService', () => { let service: ProjectsService; diff --git a/test/users/users.service.spec.ts b/test/users/users.service.spec.ts index c4bcc30..085f4a3 100644 --- a/test/users/users.service.spec.ts +++ b/test/users/users.service.spec.ts @@ -1,14 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { UsersService } from '../../src/users/users.service'; -import { - User, - UserRole, - KYCStatus, -} from '../../src/users/entities/user.entity'; -import { UpdateUserDto } from '../../src/users/dtos/update-user.dto'; + +import { UpdateUserDto } from '../../src/users/dto/update-user.dto'; import { NotFoundException, ConflictException } from '@nestjs/common'; +import { UsersService } from 'src/users/providers/users.service'; +import { User } from 'src/users/entities/user.entity'; +import { UserRole } from 'src/common/enums/user-role.enum'; +import { KYCStatus } from 'src/common/enums/kyc-status.enum'; describe('UsersService', () => { let service: UsersService; @@ -69,6 +68,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-15'), + deletedAt: null, }; describe('success scenarios', () => { @@ -96,6 +96,7 @@ describe('UsersService', () => { kycSubmittedAt: null, kycVerifiedAt: null, kycDocumentUrl: null, + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(minimalUser); @@ -178,6 +179,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; describe('success scenarios', () => { @@ -280,6 +282,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; describe('success scenarios', () => { @@ -307,6 +310,7 @@ describe('UsersService', () => { ...mockUser, walletAddress: 'GAA2M7F4E3C4D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6', + deletedAt: null, }; const newWalletAddress = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQ5G3LKYVW4R6IGPCBQVZB'; @@ -399,6 +403,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }); describe('success scenarios', () => { @@ -669,6 +674,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-15'), + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(mockUser); @@ -719,6 +725,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(mockUser); @@ -756,6 +763,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(mockUser); @@ -793,6 +801,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(mockUser); @@ -828,6 +837,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(mockUser); @@ -863,6 +873,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(mockUser); @@ -921,6 +932,7 @@ describe('UsersService', () => { kycRejectionReason: 'Document unclear', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(mockUser); @@ -959,6 +971,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(mockUser); @@ -997,6 +1010,7 @@ describe('UsersService', () => { kycRejectionReason: null, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), + deletedAt: null, }; mockRepository.findOne.mockResolvedValue(mockUser);