mirror of
https://github.com/dadosfera/maestro.git
synced 2026-08-31 19:58:21 +00:00
The JWT `permissions` claim is an array of numeric seqids at runtime (see authentication.guard.ts / authentication.decorator.ts), not claim strings. deriveOrchestIdentity previously matched claim strings against this numeric array, so roles[]/modules[] were always empty for every real user. - deriveOrchestIdentity now takes number[] | undefined and matches seqids sourced from PERMISSIONS_GROUPS (permissions.enum.ts) instead of hand-copied literals. - permissions is translated back to claim strings via a full seqid->claim catalog built once from PERMISSIONS_GROUPS; unknown seqids are dropped (auth-server ignores permissions[] in v1). - auth.controller.ts's api-key branch literal is now annotated `: UserDTO` so tsc enforces the three fields there. - Both spec files re-fixtured with numeric seqid inputs, including a mixed admin+module case and an exact claim-string translation assertion. Co-Authored-By: WOZCODE <contact@withwoz.com>
539 lines
15 KiB
TypeScript
539 lines
15 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Headers,
|
|
Post,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Inject,
|
|
UseFilters,
|
|
Get,
|
|
UseGuards,
|
|
Redirect,
|
|
Req,
|
|
Param,
|
|
Res,
|
|
UnauthorizedException,
|
|
} 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 { UserDTO } from './dtos/login';
|
|
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, Response } from 'express';
|
|
import ErrorCodes, { OauthErrors } from 'src/utils/errorCodes';
|
|
import jwt, { JwtPayload } from 'jsonwebtoken';
|
|
import { LanguageEnum } from 'src/utils/languages.enum';
|
|
import { Language } from 'src/decorators/language.decorator';
|
|
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
|
|
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
|
|
|
|
type CookiesValues = {
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
userId?: string
|
|
}
|
|
|
|
@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,
|
|
private apiKeyService: ApiKeyService,
|
|
) {
|
|
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,
|
|
@Res() res: Response,
|
|
) {
|
|
try {
|
|
this.logger.info('/auth - SignIn');
|
|
const metadata = PackTheMetadata({ language });
|
|
this.logger.info('metadata: ' + JSON.stringify(metadata.toJSON()));
|
|
const data = await this.authClient.signIn({ username, password, totp }, metadata);
|
|
|
|
if (data.tokens) {
|
|
this.authClient.writeAuthSession(res, {
|
|
accessToken: data.tokens.accessToken,
|
|
refreshToken: data.tokens.refreshToken,
|
|
userId: data.user.id
|
|
});
|
|
}
|
|
|
|
return res.send(data);
|
|
} catch (error) {
|
|
this.logger.error('/auth - SignIn - ERROR', error);
|
|
throw error;
|
|
}
|
|
|
|
}
|
|
|
|
@Post('sign-out')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
async signOut(
|
|
@Language() language: LanguageEnum,
|
|
@Res() res: Response,
|
|
) {
|
|
try {
|
|
this.logger.info('/auth - SignOut');
|
|
|
|
this.authClient.cleanUpAuthSession(res);
|
|
|
|
return res.send();
|
|
} catch (error) {
|
|
this.logger.error('/auth - SignIn - ERROR', error);
|
|
}
|
|
|
|
}
|
|
|
|
@Post('refresh-access-token')
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiOkResponse({ type: AuthRefreshAccessTokenRes })
|
|
async refreshAccessToken(
|
|
@Body() body: AuthRefreshAccessTokenReq,
|
|
@Language() language: LanguageEnum,
|
|
@Headers('origin') origin: string,
|
|
@Res() res: Response,
|
|
) {
|
|
this.logger.info('/auth - RefreshAccessToken');
|
|
const frontHost = origin.replace(/^https?:\/\//, '');
|
|
const { refreshToken, userId } = body;
|
|
|
|
const metadata = PackTheMetadata({
|
|
language,
|
|
custom_host: frontHost,
|
|
});
|
|
|
|
const data = await this.authClient.refreshAccessToken({ refreshToken, userId }, metadata);
|
|
|
|
this.authClient.writeAuthSession(res, {
|
|
accessToken: data.accessToken,
|
|
refreshToken: data.refreshToken,
|
|
userId
|
|
});
|
|
|
|
return res.send(data);
|
|
}
|
|
|
|
@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, totpCode } = body;
|
|
const { authorization: accessToken } = headers;
|
|
|
|
return this.authClient.changePassword({
|
|
accessToken,
|
|
oldPassword,
|
|
newPassword,
|
|
totpCode,
|
|
});
|
|
}
|
|
|
|
@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;
|
|
|
|
await this.authClient.resetPassword({ username }, metadata);
|
|
return { authProvider: process.env.AUTH_PROVIDER || 'cognito' };
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
@Get('me')
|
|
async getMe(@Req() req: Request, @Res() res: Response) {
|
|
this.logger.info('GET /auth/me ')
|
|
this.logger.info(JSON.stringify(req.headers));
|
|
|
|
// Check for API key header first
|
|
const apiKey = req.get('X-Api-key');
|
|
if (apiKey) {
|
|
this.logger.info('Authenticating via X-Api-key header');
|
|
const { api_key } = await this.apiKeyService.get(apiKey);
|
|
|
|
const userDto: UserDTO = {
|
|
id: api_key.user_id,
|
|
name: api_key.username,
|
|
email: api_key.username,
|
|
customer: {
|
|
id: api_key.customer_id,
|
|
name: api_key.customer_name,
|
|
tier: api_key.customer_tier,
|
|
},
|
|
permissions: [],
|
|
roles: [],
|
|
modules: [],
|
|
};
|
|
|
|
return res.status(200).json(userDto);
|
|
}
|
|
|
|
// Get token and headers
|
|
const accessToken = req.cookies['ddf-auth'];
|
|
const refreshToken = req.cookies['ddf-refresh-auth'];
|
|
const userId = req.cookies['ddf-user-id'];
|
|
const resourceHost = req.headers["x-original-url"] as string || "" ;
|
|
|
|
const hasUserSession = Boolean(accessToken) && Boolean(userId);
|
|
this.logger.info('Has User Session: ' + hasUserSession);
|
|
|
|
if (!hasUserSession) {
|
|
throw new UnauthorizedException()
|
|
}
|
|
|
|
try {
|
|
const userDto = await this.authClient.validateUserSession(accessToken, resourceHost);
|
|
return res.status(200).json(userDto);
|
|
} catch (error) {
|
|
|
|
if (!refreshToken) {
|
|
this.logger.error('Invalid refresh token or customer name');
|
|
throw new UnauthorizedException("Invalid refresh token or customer name");
|
|
};
|
|
|
|
const {
|
|
authSession,
|
|
user
|
|
} = await this.authClient.refreshUserSession(refreshToken, userId, resourceHost);
|
|
this.authClient.writeAuthSession(res, authSession);
|
|
return res.status(200).json(user);
|
|
}
|
|
}
|
|
}
|