feat(auth): implement complete Google OAuth authentication system
- Add authentication module with Google OAuth 2.0 and JWT strategies - Create secure user management with email hashing (SHA-256) - Implement rate limiting (10 requests/minute) for auth endpoints - Add CSRF protection and security middleware - Create user registration with Basic plan (50 quota default) - Add JWT-based session management with secure cookies - Implement protected routes with authentication guards - Add comprehensive API documentation with Swagger - Configure environment variables for OAuth and security - Add user profile management and quota tracking Resolves authentication requirements §18-20: - §18: Google OAuth 2.0 with email scope only - §19: Auto-create User record on first OAuth callback - §20: Store only Google UID, display name, and email hash 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e7e09d5e2c
commit
9514a2d0a3
20 changed files with 1833 additions and 41 deletions
230
packages/api/src/users/users.controller.ts
Normal file
230
packages/api/src/users/users.controller.ts
Normal file
|
@ -0,0 +1,230 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
Req,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
ApiParam,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
import { UsersService } from './users.service';
|
||||
import { JwtAuthGuard } from '../auth/auth.guard';
|
||||
import {
|
||||
UpdateUserDto,
|
||||
UserResponseDto,
|
||||
UserStatsDto
|
||||
} from './users.entity';
|
||||
|
||||
export interface AuthenticatedRequest {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
plan: string;
|
||||
quotaRemaining: number;
|
||||
isActive: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ApiTags('Users')
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class UsersController {
|
||||
private readonly logger = new Logger(UsersController.name);
|
||||
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get('me')
|
||||
@ApiOperation({
|
||||
summary: 'Get current user profile',
|
||||
description: 'Returns the authenticated user\'s profile information'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'User profile retrieved successfully',
|
||||
type: UserResponseDto
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: 'Unauthorized'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'User not found'
|
||||
})
|
||||
async getProfile(@Req() req: AuthenticatedRequest): Promise<UserResponseDto> {
|
||||
return await this.usersService.getProfile(req.user.id);
|
||||
}
|
||||
|
||||
@Put('me')
|
||||
@ApiOperation({
|
||||
summary: 'Update current user profile',
|
||||
description: 'Updates the authenticated user\'s profile information'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'User profile updated successfully',
|
||||
type: UserResponseDto
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Invalid update data'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: 'Unauthorized'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'User not found'
|
||||
})
|
||||
async updateProfile(
|
||||
@Req() req: AuthenticatedRequest,
|
||||
@Body() updateData: UpdateUserDto,
|
||||
): Promise<UserResponseDto> {
|
||||
this.logger.log(`User ${req.user.email} updating profile`);
|
||||
return await this.usersService.updateProfile(req.user.id, updateData);
|
||||
}
|
||||
|
||||
@Get('me/stats')
|
||||
@ApiOperation({
|
||||
summary: 'Get current user statistics',
|
||||
description: 'Returns usage statistics for the authenticated user'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'User statistics retrieved successfully',
|
||||
type: UserStatsDto
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: 'Unauthorized'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'User not found'
|
||||
})
|
||||
async getUserStats(@Req() req: AuthenticatedRequest): Promise<UserStatsDto> {
|
||||
return await this.usersService.getUserStats(req.user.id);
|
||||
}
|
||||
|
||||
@Delete('me')
|
||||
@ApiOperation({
|
||||
summary: 'Deactivate current user account',
|
||||
description: 'Deactivates the authenticated user\'s account (soft delete)'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'User account deactivated successfully',
|
||||
type: UserResponseDto
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: 'Unauthorized'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'User not found'
|
||||
})
|
||||
async deactivateAccount(@Req() req: AuthenticatedRequest): Promise<UserResponseDto> {
|
||||
this.logger.log(`User ${req.user.email} deactivating account`);
|
||||
return await this.usersService.deactivateAccount(req.user.id);
|
||||
}
|
||||
|
||||
@Put('me/reactivate')
|
||||
@ApiOperation({
|
||||
summary: 'Reactivate current user account',
|
||||
description: 'Reactivates the authenticated user\'s account'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'User account reactivated successfully',
|
||||
type: UserResponseDto
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: 'Unauthorized'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'User not found'
|
||||
})
|
||||
async reactivateAccount(@Req() req: AuthenticatedRequest): Promise<UserResponseDto> {
|
||||
this.logger.log(`User ${req.user.email} reactivating account`);
|
||||
return await this.usersService.reactivateAccount(req.user.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({
|
||||
summary: 'Get user by ID',
|
||||
description: 'Returns user information by ID (admin/internal use)'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'User unique identifier',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'User retrieved successfully',
|
||||
type: UserResponseDto
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: 'Unauthorized'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'User not found'
|
||||
})
|
||||
async findOne(@Param('id') id: string): Promise<UserResponseDto> {
|
||||
return await this.usersService.findOne(id);
|
||||
}
|
||||
|
||||
@Get('me/quota/check')
|
||||
@ApiOperation({
|
||||
summary: 'Check user quota availability',
|
||||
description: 'Checks if the user has sufficient quota for operations'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Quota check completed',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
hasQuota: { type: 'boolean', example: true },
|
||||
quotaRemaining: { type: 'number', example: 45 },
|
||||
quotaUsed: { type: 'number', example: 5 },
|
||||
totalQuota: { type: 'number', example: 50 },
|
||||
plan: { type: 'string', example: 'BASIC' },
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: 'Unauthorized'
|
||||
})
|
||||
async checkQuota(@Req() req: AuthenticatedRequest) {
|
||||
const hasQuota = await this.usersService.hasQuota(req.user.id);
|
||||
const stats = await this.usersService.getUserStats(req.user.id);
|
||||
|
||||
return {
|
||||
hasQuota,
|
||||
quotaRemaining: req.user.quotaRemaining,
|
||||
quotaUsed: stats.quotaUsed,
|
||||
totalQuota: stats.totalQuota,
|
||||
plan: req.user.plan,
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue