SEO_iamge_renamer_starting_.../packages/api/src/auth/auth.controller.ts
DustyWalker 9514a2d0a3 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>
2025-08-05 17:09:43 +02:00

235 lines
No EOL
6.2 KiB
TypeScript

import {
Controller,
Get,
Post,
UseGuards,
Req,
Res,
HttpStatus,
HttpException,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiExcludeEndpoint,
} from '@nestjs/swagger';
import { User } from '@prisma/client';
import { AuthService } from './auth.service';
import { GoogleAuthGuard, JwtAuthGuard, Public } from './auth.guard';
import {
LoginResponseDto,
LogoutResponseDto,
AuthProfileDto
} from './dto/auth.dto';
export interface AuthenticatedRequest extends Request {
user: User;
}
@ApiTags('Authentication')
@Controller('auth')
export class AuthController {
private readonly logger = new Logger(AuthController.name);
constructor(private readonly authService: AuthService) {}
@Get('google')
@Public()
@UseGuards(GoogleAuthGuard)
@ApiOperation({
summary: 'Initiate Google OAuth authentication',
description: 'Redirects user to Google OAuth consent screen'
})
@ApiResponse({
status: 302,
description: 'Redirect to Google OAuth'
})
@ApiExcludeEndpoint() // Don't show in Swagger UI as it's a redirect
async googleAuth() {
// Guard handles the redirect to Google
// This method exists for the decorator
}
@Get('google/callback')
@Public()
@UseGuards(GoogleAuthGuard)
@ApiOperation({
summary: 'Google OAuth callback',
description: 'Handles the callback from Google OAuth and creates/logs in user'
})
@ApiResponse({
status: 200,
description: 'Authentication successful',
type: LoginResponseDto
})
@ApiResponse({
status: 401,
description: 'Authentication failed'
})
@ApiExcludeEndpoint() // Don't show in Swagger UI as it's a callback
async googleCallback(
@Req() req: AuthenticatedRequest,
@Res() res: Response,
) {
try {
if (!req.user) {
throw new HttpException('Authentication failed', HttpStatus.UNAUTHORIZED);
}
// Generate JWT tokens for the authenticated user
const tokenData = await this.authService.generateTokens(req.user);
// Get frontend URL from config
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
// Set secure HTTP-only cookie with the JWT token
res.cookie('access_token', tokenData.accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: tokenData.expiresIn * 1000, // Convert to milliseconds
path: '/',
});
// Redirect to frontend with success indication
const redirectUrl = `${frontendUrl}/auth/success?user=${encodeURIComponent(
JSON.stringify({
id: tokenData.user.id,
email: tokenData.user.email,
plan: tokenData.user.plan,
quotaRemaining: tokenData.user.quotaRemaining,
})
)}`;
this.logger.log(`User ${req.user.email} authenticated successfully`);
return res.redirect(redirectUrl);
} catch (error) {
this.logger.error('OAuth callback error:', error);
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
return res.redirect(`${frontendUrl}/auth/error?message=${encodeURIComponent('Authentication failed')}`);
}
}
@Post('logout')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: 'Logout user',
description: 'Invalidates the user session and clears authentication cookies'
})
@ApiResponse({
status: 200,
description: 'Successfully logged out',
type: LogoutResponseDto
})
@ApiResponse({
status: 401,
description: 'Unauthorized'
})
async logout(
@Req() req: AuthenticatedRequest,
@Res() res: Response,
): Promise<Response> {
try {
const result = await this.authService.logout(req.user.id);
// Clear the authentication cookie
res.clearCookie('access_token', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
});
this.logger.log(`User ${req.user.email} logged out successfully`);
return res.status(HttpStatus.OK).json(result);
} catch (error) {
this.logger.error('Logout error:', error);
throw new HttpException('Logout failed', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Get('profile')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: 'Get current user profile',
description: 'Returns the authenticated user\'s profile information'
})
@ApiResponse({
status: 200,
description: 'User profile retrieved successfully',
type: AuthProfileDto
})
@ApiResponse({
status: 401,
description: 'Unauthorized'
})
async getProfile(@Req() req: AuthenticatedRequest): Promise<AuthProfileDto> {
try {
const user = await this.authService.getProfile(req.user.id);
return {
id: user.id,
email: user.email,
plan: user.plan,
quotaRemaining: user.quotaRemaining,
quotaResetDate: user.quotaResetDate,
isActive: user.isActive,
createdAt: user.createdAt,
};
} catch (error) {
this.logger.error('Get profile error:', error);
throw new HttpException('Failed to retrieve profile', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Get('status')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: 'Check authentication status',
description: 'Verifies if the current JWT token is valid'
})
@ApiResponse({
status: 200,
description: 'Token is valid',
schema: {
type: 'object',
properties: {
authenticated: { type: 'boolean', example: true },
user: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
plan: { type: 'string' },
}
}
}
}
})
@ApiResponse({
status: 401,
description: 'Token is invalid or expired'
})
async checkStatus(@Req() req: AuthenticatedRequest) {
return {
authenticated: true,
user: {
id: req.user.id,
email: req.user.email,
plan: req.user.plan,
quotaRemaining: req.user.quotaRemaining,
},
};
}
}