import { Body, Controller, Headers, Post, HttpCode, HttpStatus, Inject, UseFilters, Get, UseGuards, Redirect, Req, Param, } from '@nestjs/common'; import { ApiHeaders, ApiOkResponse, ApiSecurity, ApiTags, } from '@nestjs/swagger'; import { AuthChangePasswordRequest, AuthResetPasswordRequest, AuthVerifyResetPasswordCodeRequest, AuthConfirmResetPasswordRequest, AuthEnableTotpMfaRequest, AuthDisableTotpMfaRequest, AuthVerifyTotpMfaRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages'; import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum'; import { Authenticated, RequireAllPermissions, } from 'src/decorators/authentication.decorator'; import { AuthClientService } from './auth.service'; import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; import { GrpcToHttpExceptionFilter } from '../../error/grpc-to-http-exception.filter'; import { RequestUser, User } from 'src/decorators/user.decorator'; import { AuthRefreshAccessTokenReq, AuthRefreshAccessTokenRes, AuthSignInReq, AuthSignInRes, BulkEditRequest, } from './dtos/login'; import { PackTheMetadata } from 'src/utils/PackTheMetadata'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import ErrorCodes, { OauthErrors } from 'src/utils/errorCodes'; import jwt from 'jsonwebtoken'; import { LanguageEnum } from 'src/utils/languages.enum'; import { Language } from 'src/decorators/language.decorator'; import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator'; @ApiTags('Auth') @ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }]) @UseFilters(new GrpcToHttpExceptionFilter()) @Controller('auth') export class AuthController { logger: DadosferaLogger; redirectUrl: string; constructor( @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, private authClient: AuthClientService, ) { this.logger = dadosferaLogger.logger; switch (process.env.ENV) { case 'stg': this.redirectUrl = `https://app.${process.env.ENV}.dadosfera.ai/auth/login`; break; case 'prd': this.redirectUrl = `https://app.dadosfera.ai/auth/login`; break; default: this.redirectUrl = `http://localhost:4200/auth/login`; } } @Post('sign-in') @HttpCode(HttpStatus.OK) async signIn( @Body() { username, password, totp }: AuthSignInReq, @Language() language: LanguageEnum, ): Promise { this.logger.info('/auth - SignIn'); const metadata = PackTheMetadata({ language }); this.logger.info('metadata: ' + JSON.stringify(metadata.toJSON())); return this.authClient.signIn({ username, password, totp }, metadata); } @Post('refresh-access-token') @HttpCode(HttpStatus.OK) @ApiOkResponse({ type: AuthRefreshAccessTokenRes }) async refreshAccessToken( @Body() body: AuthRefreshAccessTokenReq, @Language() language: LanguageEnum, @Headers('origin') origin: string, ) { this.logger.info('/auth - RefreshAccessToken'); const { refreshToken, userId } = body; const frontHost = origin.replace(/^https?:\/\//, ''); const metadata = PackTheMetadata({ language, custom_host: frontHost, }); return this.authClient.refreshAccessToken({ refreshToken, userId }, metadata); } @ApiInternalOnlyEndpoint() @Post('/sso/snowflake') @RequireAllPermissions(PERMISSIONS_GROUPS.SNOWFLAKE.permissions.OPEN) @HttpCode(HttpStatus.OK) async snowflakeSignIn( @User() user: RequestUser, @Body('RelayState') relayState: string, ) { this.logger.info('/auth - snowflakeSignIn'); return this.authClient.snowflakeSignIn({ userId: user.user_id, relayState, }); } @ApiInternalOnlyEndpoint() @Post('change-password') @HttpCode(HttpStatus.OK) async changePassword( @Body() body: AuthChangePasswordRequest, @Headers() headers, ) { this.logger.info('/auth - change-password'); const { oldPassword, newPassword } = body; const { authorization: accessToken } = headers; return this.authClient.changePassword({ accessToken, oldPassword, newPassword, }); } @ApiInternalOnlyEndpoint() @Post('reset-password') @HttpCode(HttpStatus.OK) async resetPassword( @Body() body: AuthResetPasswordRequest, @Language() language: LanguageEnum, ) { this.logger.info('/auth - reset-password'); const metadata = PackTheMetadata({ language: language, }); const { username } = body; return this.authClient.resetPassword({ username }, metadata); } @ApiInternalOnlyEndpoint() @Post('verify-reset-password-code') @HttpCode(HttpStatus.OK) async verifyResetPasswordCode( @Body() body: AuthVerifyResetPasswordCodeRequest, ) { this.logger.info('/auth - verify-reset-password-code'); const { username, code } = body; return this.authClient.verifyResetPasswordCode({ username, code }); } @ApiInternalOnlyEndpoint() @Post('confirm-reset-password') @HttpCode(HttpStatus.OK) async confirmResetPassword( @Body() body: AuthConfirmResetPasswordRequest, @Headers('origin') origin: string, ) { this.logger.info('/auth - confirm-reset-password'); const frontHost = origin.replace(/^https?:\/\//, ''); const metadata = PackTheMetadata({ custom_host: frontHost }); const { username, code, newPassword } = body; return this.authClient.confirmResetPassword( { username, code, newPassword, }, metadata, ); } @ApiInternalOnlyEndpoint() @Post('enable-totp') @HttpCode(HttpStatus.OK) async enableTotpMFA( @Body() body: AuthEnableTotpMfaRequest, @Headers() headers, ) { this.logger.info('/auth - enable-totp'); const { password } = body; const { authorization: accessToken } = headers; return this.authClient.enableTotpMFA({ accessToken, password }); } @ApiInternalOnlyEndpoint() @Post('disable-totp') @HttpCode(HttpStatus.OK) async disableTotpMFA( @Body() body: AuthDisableTotpMfaRequest, @Headers() headers, ) { this.logger.info('/auth - disable-totp'); const { password } = body; const { authorization: accessToken } = headers; return this.authClient.disableTotpMFA({ accessToken, password }); } @ApiInternalOnlyEndpoint() @Post('dismiss-totp') @HttpCode(HttpStatus.OK) async dismissTotpMFA(@Headers() headers) { this.logger.info('/auth - disable-totp'); const { authorization: accessToken } = headers; return this.authClient.dismissTotpMFA({ accessToken }); } @ApiInternalOnlyEndpoint() @Post('verify-totp') @HttpCode(HttpStatus.OK) async verifyTotp(@Body() body: AuthVerifyTotpMfaRequest, @Headers() headers) { this.logger.info('/auth - enable-totp'); const { totp } = body; const { authorization: accessToken } = headers; return this.authClient.verifyTotp({ accessToken, totp }); } @ApiInternalOnlyEndpoint() @Authenticated() @ApiSecurity('access-token') @Get('verify-access-token') verifyAccessToken() { return { access_token_status: 'valid' }; } @ApiInternalOnlyEndpoint() @Get('session/:session') getSession(@Param('session') session: string) { return this.authClient.getSession(session); } @ApiInternalOnlyEndpoint() @Get('oauth/google') @UseGuards(AuthGuard('google-login')) googleOauth() { this.logger.info('/oauth/google'); return true; } @ApiInternalOnlyEndpoint() @Get('oauth/google/callback') @UseGuards(AuthGuard('google-login')) @Redirect() async googleOauthCallback(@Req() req) { const { url, email, token, language = 'pt-br' } = await this.callback(req); if (url.searchParams.get('error')) { this.logger.error('/oauth/google - ERROR'); return { url: url.href }; } await this.authClient .oauthSignIn({ username: email, token, }) .then(({ session }) => { this.logger.info('/oauth/google - SUCESS'); url.searchParams.set('session', session); }) .catch((err) => { this.logger.error('/oauth/google - ERROR'); let error = OauthErrors.INVALID_CREDENTIALS[language].error; let error_description = OauthErrors.INVALID_CREDENTIALS[language].error_description; switch (err.details) { case ErrorCodes.USER.NOT_FOUND: error = OauthErrors.USER_NOT_FOUND[language].error; error_description = OauthErrors.USER_NOT_FOUND[language].error_description(email); break; case ErrorCodes.AUTH.UNAUTHORIZED: error = OauthErrors.INVALID_SESSION[language].error; error_description = OauthErrors.INVALID_SESSION[language].error_description; break; } url.searchParams.set('error', error); url.searchParams.set('error_description', error_description); return null; }); return { url: url.href }; } async callback(req: Request) { const { error, state } = req.query; const { authInfo } = req; const url = new URL(this.redirectUrl); let email, token, error_title, error_description; let language: 'pt-br' | 'en-us' = 'pt-br'; const stateObject = jwt.verify( state as string, process.env.JWT_PRIVATE_KEY, ); if (typeof stateObject != 'string') language = stateObject.language; if (error || !authInfo) { this.logger.error(error); if (!authInfo) this.logger.error('No authInfo', { request: req }); error_title = OauthErrors.INVALID_CREDENTIALS[language].error; error_description = OauthErrors.INVALID_CREDENTIALS[language].error_description; if (error) error_description += ` - [${error}]`; } else { const { accessToken } = authInfo as any; const { _json: userInfo } = req.user as any; email = userInfo.email; token = accessToken; } if (error_title) { url.searchParams.set('error', error_title); url.searchParams.set('error_description', error_description); } return { token, email, url, language }; } @ApiInternalOnlyEndpoint() @Post('users/block') @RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN) @HttpCode(HttpStatus.OK) async blockUsers( @Language() language: LanguageEnum, @User() user: RequestUser, @Body() body: BulkEditRequest, ) { this.logger.info('blockUsers - Starting request'); try { const metadata = PackTheMetadata(user); this.logger.debug('Calling blockUsers service', { metadata: { access_token: metadata.get('access_token'), language: metadata.get('language'), }, }); const result = await this.authClient.blockUsers(body.users, metadata); this.logger.info('blockUsers - Success', { result }); return result; } catch (error) { this.logger.error('blockUsers - Error', { error: error.message, stack: error.stack, }); throw error; } } @ApiInternalOnlyEndpoint() @Post('users/unblock') @RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN) @HttpCode(HttpStatus.OK) async unblockUsers( @Language() language: LanguageEnum, @User() user: RequestUser, @Body() body: BulkEditRequest, ) { this.logger.info('unblockUsers'); const metadata = PackTheMetadata(user); return this.authClient.unblockUsers(body.users, metadata); } @ApiInternalOnlyEndpoint() @Post('users/reset') @RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN) @HttpCode(HttpStatus.OK) async resetUsers( @Language() language: LanguageEnum, @User() user: RequestUser, @Body() body: BulkEditRequest, ) { this.logger.info('resetUsers'); const metadata = PackTheMetadata(user); return this.authClient.resetUsers(body.users, metadata); } }