Files
dyolink/backend/src/modules/auth/auth.controller.ts

380 lines
10 KiB
TypeScript
Raw Normal View History

// backend/src/modules/auth/auth.controller.ts
import {
Controller,
Post,
Body,
UseGuards,
Req,
Res,
HttpCode,
HttpStatus,
Get,
Patch,
UnauthorizedException,
} from '@nestjs/common';
import type { Response } from 'express';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiBody,
ApiUnauthorizedResponse,
ApiBadRequestResponse
} from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { CreateOrganizationDto } from './dto/create-organization.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { UpdateLanguageDto } from './dto/update-language.dto';
import {
ForgotPasswordSendCodeDto,
ForgotPasswordVerifyDto,
} from './dto/forgot-password.dto';
import { ChangePasswordDto } from './dto/change-password.dto';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
2026-07-02 17:59:49 +03:30
private static readonly REMEMBER_ME_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
private static readonly PASSWORD_RESET_MAX_AGE_MS = 15 * 60 * 1000;
2026-07-02 17:59:49 +03:30
constructor(private readonly authService: AuthService) {}
// =========================
// LOGIN
// =========================
@Post('login')
@HttpCode(HttpStatus.OK)
@UseGuards(LocalAuthGuard)
@ApiOperation({ summary: 'Login with email and password' })
@ApiBody({ type: LoginDto })
@ApiResponse({ status: 200, description: 'Login successful' })
@ApiUnauthorizedResponse({ description: 'Invalid credentials' })
@ApiBadRequestResponse({ description: 'Invalid input data' })
async login(
@Body() loginDto: LoginDto,
@Req() req,
@Res({ passthrough: true }) res: Response
) {
console.log('Login endpoint hit');
const result = await this.authService.login(loginDto, req.user);
2026-07-02 17:59:49 +03:30
const rememberMe = Boolean(loginDto.rememberMe);
2026-07-02 17:59:49 +03:30
this.setAuthCookies(
res,
result.data.accessToken,
result.data.refreshToken,
rememberMe,
);
return {
success: true,
data: {
user: result.data.user,
organizations: result.data.organizations,
},
};
}
// =========================
// REGISTER
// =========================
@Post('register')
@ApiOperation({ summary: 'Register a new user' })
@ApiBody({ type: RegisterDto })
@ApiResponse({ status: 201, description: 'User registered successfully' })
@ApiBadRequestResponse({ description: 'Invalid input data' })
async register(
@Body() registerDto: RegisterDto,
@Res({ passthrough: true }) res: Response
) {
console.log('Register endpoint hit');
const result = await this.authService.register(registerDto);
// ✅ SET COOKIES HERE
this.setAuthCookies(res, result.data.accessToken, result.data.refreshToken);
return {
success: true,
data: {
user: result.data.user,
organizations: result.data.organizations,
},
};
}
// =========================
// SELECT ORGANIZATION
// =========================
@Post('select-organization')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
async selectOrganization(
@Req() req,
@Body('organizationId') organizationId: string,
@Res({ passthrough: true }) res: Response
) {
const result = await this.authService.selectOrganization(
req.user.id,
organizationId
);
2026-07-02 17:59:49 +03:30
this.setAccessToken(
res,
result.data.accessToken,
this.isPersistentSession(req),
);
return {
success: true,
data: {
organization: result.data.organization,
},
};
}
@Post('organizations')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Create organization for current user' })
async createOrganization(@Req() req, @Body() dto: CreateOrganizationDto) {
return this.authService.createOrganization(
req.user.id,
req.user.organizationId,
dto,
);
}
// =========================
// PROFILE
// =========================
@Get('profile')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Get user profile' })
@ApiResponse({ status: 200, description: 'Profile retrieved successfully' })
@ApiUnauthorizedResponse({ description: 'Invalid or missing JWT token' })
async getProfile(@Req() req) {
console.log('Profile endpoint hit');
console.log('USER FROM JWT:', req.user);
return this.authService.getProfile(req.user.id);
}
@Patch('profile/language')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Update user language preference' })
async updateLanguage(@Req() req, @Body() dto: UpdateLanguageDto) {
return this.authService.updateLanguage(req.user.id, dto);
}
@Patch('profile/password')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Change account password' })
async changePassword(
@Req() req,
@Body() dto: ChangePasswordDto,
@Res({ passthrough: true }) res: Response,
) {
const skipCurrentPassword = req?.cookies?.passwordResetVerified === '1';
const result = await this.authService.changePassword(
req.user.id,
dto.currentPassword,
dto.newPassword,
skipCurrentPassword,
);
this.clearAuthCookies(res);
res.clearCookie('passwordResetVerified', this.baseCookieOptions());
return result;
}
@Post('forgot-password/send-code')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Send forgot-password SMS verification code' })
async sendForgotPasswordCode(@Body() dto: ForgotPasswordSendCodeDto) {
return this.authService.sendForgotPasswordCode(dto);
}
@Post('forgot-password/verify')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Verify SMS code and sign in for password reset' })
async verifyForgotPasswordCode(
@Body() dto: ForgotPasswordVerifyDto,
@Res({ passthrough: true }) res: Response,
) {
const result = await this.authService.verifyForgotPasswordCode(dto);
this.setAuthCookies(
res,
result.data.accessToken,
result.data.refreshToken,
);
res.cookie('passwordResetVerified', '1', {
...this.baseCookieOptions(),
maxAge: AuthController.PASSWORD_RESET_MAX_AGE_MS,
});
return {
success: true,
data: {
user: result.data.user,
organizations: result.data.organizations,
redirectTo: '/settings/account',
},
};
}
@Get('subscription-alert')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({
summary:
'Owner-only: seat / trial status for warning indicator (current org from JWT)',
})
async getSubscriptionAlert(@Req() req) {
return this.authService.getOwnerSubscriptionAlert(
req.user.id,
req.user.organizationId,
);
}
// =========================
// REFRESH
// =========================
@Post('refresh')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Refresh access token using refresh cookie' })
@ApiResponse({ status: 200, description: 'Access token refreshed' })
@ApiUnauthorizedResponse({ description: 'Invalid or missing refresh token' })
async refresh(@Req() req, @Res({ passthrough: true }) res: Response) {
const refreshToken = req?.cookies?.refreshToken;
if (!refreshToken) {
throw new UnauthorizedException('Refresh token not found');
}
const result = await this.authService.refreshToken(refreshToken);
2026-07-02 17:59:49 +03:30
this.setAccessToken(
res,
result.data.accessToken,
this.isPersistentSession(req),
);
return {
success: true,
data: {
accessToken: result.data.accessToken,
},
};
}
2026-04-29 15:55:58 +03:30
// =========================
// LOGOUT
// =========================
@Post('logout')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Logout current user' })
@ApiResponse({ status: 200, description: 'Logout successful' })
async logout(@Req() req, @Res({ passthrough: true }) res: Response) {
const accessToken = req?.cookies?.accessToken;
if (accessToken) {
await this.authService.logout(accessToken);
}
this.clearAuthCookies(res);
return {
success: true,
message: 'Logged out successfully',
};
}
// =========================
// TEST
// =========================
@Get('test')
@ApiOperation({ summary: 'Test endpoint' })
test() {
return { message: 'Auth controller is working!' };
}
// =========================
// 🔥 COOKIE HELPERS
// =========================
2026-07-02 17:59:49 +03:30
private isPersistentSession(req: { cookies?: Record<string, string> }): boolean {
return req?.cookies?.authRemember === '1';
}
private baseCookieOptions() {
return {
httpOnly: true,
secure: false, // ⚠️ true in production (HTTPS)
sameSite: 'lax' as const,
path: '/',
};
}
private setAuthCookies(
res: Response,
accessToken: string,
2026-07-02 17:59:49 +03:30
refreshToken: string,
rememberMe = false,
) {
2026-07-02 17:59:49 +03:30
this.setAccessToken(res, accessToken, rememberMe);
this.setRefreshToken(res, refreshToken, rememberMe);
this.setRememberMeFlag(res, rememberMe);
}
2026-07-02 17:59:49 +03:30
private setAccessToken(res: Response, token: string, rememberMe = false) {
res.cookie('accessToken', token, {
2026-07-02 17:59:49 +03:30
...this.baseCookieOptions(),
...(rememberMe
? { maxAge: AuthController.REMEMBER_ME_MAX_AGE_MS }
: {}),
});
}
2026-07-02 17:59:49 +03:30
private setRefreshToken(res: Response, token: string, rememberMe = false) {
res.cookie('refreshToken', token, {
2026-07-02 17:59:49 +03:30
...this.baseCookieOptions(),
...(rememberMe
? { maxAge: AuthController.REMEMBER_ME_MAX_AGE_MS }
: {}),
});
}
2026-04-29 15:55:58 +03:30
2026-07-02 17:59:49 +03:30
private setRememberMeFlag(res: Response, rememberMe: boolean) {
if (rememberMe) {
res.cookie('authRemember', '1', {
...this.baseCookieOptions(),
maxAge: AuthController.REMEMBER_ME_MAX_AGE_MS,
});
return;
}
res.clearCookie('authRemember', this.baseCookieOptions());
}
2026-04-29 15:55:58 +03:30
private clearAuthCookies(res: Response) {
2026-07-02 17:59:49 +03:30
const options = this.baseCookieOptions();
res.clearCookie('accessToken', options);
res.clearCookie('refreshToken', options);
res.clearCookie('authRemember', options);
res.clearCookie('passwordResetVerified', options);
2026-04-29 15:55:58 +03:30
}
}