import { Body, Controller, Get, Post, Query, Request, Res, UseGuards, UseInterceptors, } from '@nestjs/common'; import { AuthService } from './auth.service'; import { SignInMagicDto } from './dto/signin-magic.dto'; import { VerifyMagicDto } from './dto/verify-magic.dto'; import { Response } from 'express'; import * as E from 'fp-ts/Either'; import { RTJwtAuthGuard } from './guards/rt-jwt-auth.guard'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { GqlUser } from 'src/decorators/gql-user.decorator'; import { AuthUser } from 'src/types/AuthUser'; import { RTCookie } from 'src/decorators/rt-cookie.decorator'; import { AuthProvider, authCookieHandler, authProviderCheck } from './helper'; import { isValidLocalhostRedirectUri } from './redirect-uri.validator'; import { GoogleSSOGuard } from './guards/google-sso.guard'; import { GithubSSOGuard } from './guards/github-sso.guard'; import { MicrosoftSSOGuard } from './guards/microsoft-sso.guard'; import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard'; import { SkipThrottle } from '@nestjs/throttler'; import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors'; import { ConfigService } from '@nestjs/config'; import { throwHTTPErr } from 'src/utils'; import { UserLastLoginInterceptor } from 'src/interceptors/user-last-login.interceptor'; import { CreateLocalUserDto, LocalSetupAdminDto, LocalSignInDto, } from './dto/local-auth.dto'; import { LocalAuthService } from './local-auth.service'; @UseGuards(ThrottlerBehindProxyGuard) @Controller({ path: 'auth', version: '1' }) export class AuthController { constructor( private authService: AuthService, private configService: ConfigService, private localAuthService: LocalAuthService, ) {} @Get('providers') async getAuthProviders() { const providers = await this.authService.getAuthProviders(); return { providers }; } /** ** Route to initiate magic-link auth for a users email */ @Post('signin') async signInMagicLink( @Body() authData: SignInMagicDto, @Query('origin') origin: string, ) { if ( !authProviderCheck( AuthProvider.EMAIL, this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'), ) ) { throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 }); } const deviceIdToken = await this.authService.signInMagicLink( authData.email, origin, ); if (E.isLeft(deviceIdToken)) throwHTTPErr(deviceIdToken.left); return deviceIdToken.right; } /** ** Route to verify and sign in a valid user via magic-link */ @Post('verify') async verify(@Body() data: VerifyMagicDto, @Res() res: Response) { const authTokens = await this.authService.verifyMagicLinkTokens(data); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); authCookieHandler(res, authTokens.right, false, null, this.configService); } @Post('local/signin') async signInLocal(@Body() data: LocalSignInDto, @Res() res: Response) { const authTokens = await this.localAuthService.signIn(data); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); authCookieHandler(res, authTokens.right, false, null, this.configService); } @Post('local/setup-admin') async setupLocalAdmin( @Body() data: LocalSetupAdminDto, @Res() res: Response, ) { const authTokens = await this.localAuthService.setupFirstAdmin(data); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); authCookieHandler(res, authTokens.right, false, null, this.configService); } @Post('local/users') @UseGuards(JwtAuthGuard) async createLocalUser( @GqlUser() user: AuthUser, @Body() data: CreateLocalUserDto, ) { const createdUser = await this.localAuthService.createLocalUser(data, user); if (E.isLeft(createdUser)) throwHTTPErr(createdUser.left); return createdUser.right; } /** ** Route to refresh auth tokens with Refresh Token Rotation * @see https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation */ @Get('refresh') @UseGuards(RTJwtAuthGuard) async refresh( @GqlUser() user: AuthUser, @RTCookie() refresh_token: string, @Res() res, ) { const newTokenPair = await this.authService.refreshAuthTokens( refresh_token, user, ); if (E.isLeft(newTokenPair)) throwHTTPErr(newTokenPair.left); authCookieHandler(res, newTokenPair.right, false, null, this.configService); } /** ** Route to initiate SSO auth via Google */ @Get('google') @UseGuards(GoogleSSOGuard) async googleAuth(@Request() req) {} /** ** Callback URL for Google SSO * @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-it-works */ @Get('google/callback') @SkipThrottle() @UseGuards(GoogleSSOGuard) @UseInterceptors(UserLastLoginInterceptor) async googleAuthRedirect(@Request() req, @Res() res) { const authTokens = await this.authService.generateAuthTokens(req.user.uid); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); authCookieHandler( res, authTokens.right, true, req.authInfo.state.redirect_uri, this.configService, ); } /** ** Route to initiate SSO auth via Github */ @Get('github') @UseGuards(GithubSSOGuard) async githubAuth(@Request() req) {} /** ** Callback URL for Github SSO * @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-it-works */ @Get('github/callback') @SkipThrottle() @UseGuards(GithubSSOGuard) @UseInterceptors(UserLastLoginInterceptor) async githubAuthRedirect(@Request() req, @Res() res) { const authTokens = await this.authService.generateAuthTokens(req.user.uid); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); authCookieHandler( res, authTokens.right, true, req.authInfo.state.redirect_uri, this.configService, ); } /** ** Route to initiate SSO auth via Microsoft */ @Get('microsoft') @UseGuards(MicrosoftSSOGuard) async microsoftAuth(@Request() req) {} /** ** Callback URL for Microsoft SSO * @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-it-works */ @Get('microsoft/callback') @SkipThrottle() @UseGuards(MicrosoftSSOGuard) @UseInterceptors(UserLastLoginInterceptor) async microsoftAuthRedirect(@Request() req, @Res() res) { const authTokens = await this.authService.generateAuthTokens(req.user.uid); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); authCookieHandler( res, authTokens.right, true, req.authInfo.state.redirect_uri, this.configService, ); } /** ** Log user out by clearing cookies containing auth tokens */ @Get('logout') async logout(@Res() res: Response) { res.clearCookie('access_token'); res.clearCookie('refresh_token'); return res.status(200).send(); } @Get('verify/admin') @UseGuards(JwtAuthGuard) async verifyAdmin(@GqlUser() user: AuthUser) { const userInfo = await this.authService.verifyAdmin(user); if (E.isLeft(userInfo)) throwHTTPErr(userInfo.left); return userInfo.right; } @Get('desktop') @UseGuards(JwtAuthGuard) @UseInterceptors(UserLastLoginInterceptor) async desktopAuthCallback( @GqlUser() user: AuthUser, @Query('redirect_uri') redirectUri: string, ) { if (!isValidLocalhostRedirectUri(redirectUri)) { throwHTTPErr({ message: 'Invalid desktop callback URL', statusCode: 400, }); } const tokens = await this.authService.generateAuthTokens(user.uid); if (E.isLeft(tokens)) throwHTTPErr(tokens.left); return tokens.right; } @Get('verify-token') @UseGuards(JwtAuthGuard) async verifyToken(@GqlUser() user: AuthUser) { return { isValid: true, uid: user.uid, message: 'Token is valid', }; } }