FEAT: set cookie after user login

This commit is contained in:
marcos-silva-rodrigues
2025-07-03 15:52:55 -03:00
parent e5cef90a64
commit a381f50e88
5 changed files with 183 additions and 193 deletions
+28 -183
View File
@@ -34,14 +34,7 @@
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthSignInRes"
}
}
}
"description": ""
}
},
"tags": [
@@ -675,6 +668,33 @@
]
}
},
"/auth/me": {
"get": {
"operationId": "AuthController_getMe",
"parameters": [
{
"name": "dadosfera-lang",
"in": "header",
"required": false,
"schema": {
"enum": [
"pt-br",
"en-us"
],
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Auth"
]
}
},
"/connections": {
"post": {
"operationId": "ConnectionController_createConnection",
@@ -6476,181 +6496,6 @@
"password"
]
},
"AuthCustomer": {
"type": "object",
"properties": {
"modules": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"displayName": {
"type": "string"
},
"tier": {
"type": "string"
},
"scheduleLimit": {
"type": "string"
},
"links": {
"type": "array",
"items": {
"type": "string"
}
},
"themeEnabled": {
"type": "boolean"
},
"enforceMfa": {
"type": "boolean"
}
},
"required": [
"modules",
"id",
"name",
"displayName",
"tier",
"scheduleLimit",
"links",
"themeEnabled",
"enforceMfa"
]
},
"AuthUser": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"username": {
"type": "string"
},
"createdAt": {
"type": "string"
}
},
"required": [
"id",
"name",
"username",
"createdAt"
]
},
"AuthTokens": {
"type": "object",
"properties": {
"accessToken": {
"type": "string"
},
"refreshToken": {
"type": "string"
},
"termsOfUseToken": {
"type": "string"
},
"idToken": {
"type": "string",
"deprecated": true
}
},
"required": [
"accessToken",
"refreshToken",
"idToken"
]
},
"TermsOfUse": {
"type": "object",
"properties": {
"version": {
"type": "number"
},
"publicUrl": {
"type": "string"
},
"enforceDate": {
"type": "string"
},
"createdAt": {
"type": "string"
}
},
"required": [
"version",
"publicUrl",
"enforceDate",
"createdAt"
]
},
"TermsOfUseStatus": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"pending",
"required",
"ok"
]
},
"lastSigned": {
"$ref": "#/components/schemas/TermsOfUse"
},
"next": {
"$ref": "#/components/schemas/TermsOfUse"
}
},
"required": [
"status"
]
},
"AuthSignInRes": {
"type": "object",
"properties": {
"permissions": {
"type": "array",
"items": {
"type": "string"
}
},
"mfaStatus": {
"type": "string",
"enum": [
"pending",
"none",
"totp"
]
},
"customer": {
"$ref": "#/components/schemas/AuthCustomer"
},
"user": {
"$ref": "#/components/schemas/AuthUser"
},
"tokens": {
"$ref": "#/components/schemas/AuthTokens"
},
"termsOfUse": {
"$ref": "#/components/schemas/TermsOfUseStatus"
}
},
"required": [
"permissions",
"mfaStatus"
]
},
"AuthRefreshAccessTokenReq": {
"type": "object",
"properties": {
+34
View File
@@ -29,6 +29,7 @@
"axios": "^0.27.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.7",
"cron-parser": "^4.9.0",
"csv": "^6.3.11",
"dotenv": "^14.3.2",
@@ -52,6 +53,7 @@
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.9",
"@types/express": "^4.17.17",
"@types/jest": "27.0.2",
"@types/jsonwebtoken": "^8.5.9",
@@ -3664,6 +3666,16 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie-parser": {
"version": "1.4.9",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz",
"integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/express": "*"
}
},
"node_modules/@types/cookiejar": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
@@ -5740,6 +5752,28 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+2
View File
@@ -47,6 +47,7 @@
"axios": "^0.27.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.7",
"cron-parser": "^4.9.0",
"csv": "^6.3.11",
"dotenv": "^14.3.2",
@@ -73,6 +74,7 @@
"multer": "1.4.5-lts.1"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.9",
"@types/express": "^4.17.17",
"@types/jest": "27.0.2",
"@types/jsonwebtoken": "^8.5.9",
+4
View File
@@ -9,6 +9,8 @@ import { AppModule } from './app.module';
import { writeFileSync } from 'fs';
import { execSync } from 'child_process';
import { INestApplication } from '@nestjs/common';
import cookieParser from 'cookie-parser';
async function bootstrap() {
DadosferaLogger.setupLogger({
serviceName: 'maestro',
@@ -23,9 +25,11 @@ async function bootstrap() {
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
credentials: true
},
});
app.use(helmet());
app.use(cookieParser(process.env.COOKIE_SECRET));
if (process.env.ENV === 'prd') {
app.use('/catalog/register-dataset', json({ limit: '10mb' }));
app.use('/catalog/register-dataset', urlencoded({ extended: true, limit: '10mb' }));
+115 -10
View File
@@ -12,6 +12,7 @@ import {
Redirect,
Req,
Param,
Res,
} from '@nestjs/common';
import {
ApiHeaders,
@@ -47,9 +48,9 @@ import {
} from './dtos/login';
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
import { Request, Response } from 'express';
import ErrorCodes, { OauthErrors } from 'src/utils/errorCodes';
import jwt from 'jsonwebtoken';
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';
@@ -86,11 +87,20 @@ export class AuthController {
async signIn(
@Body() { username, password, totp }: AuthSignInReq,
@Language() language: LanguageEnum,
): Promise<AuthSignInRes> {
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);
@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);
this.addTokenInCookie(res, data.tokens);
return res.send(data);
} catch (error) {
this.logger.error('/auth - SignIn - ERROR', error);
}
}
@Post('refresh-access-token')
@@ -100,17 +110,23 @@ export class AuthController {
@Body() body: AuthRefreshAccessTokenReq,
@Language() language: LanguageEnum,
@Headers('origin') origin: string,
@Res() res: Response,
) {
this.logger.info('/auth - RefreshAccessToken');
const { refreshToken, customerName: customer_name } = body;
const frontHost = origin.replace(/^https?:\/\//, '');
const metadata = PackTheMetadata({
language,
customer_name,
custom_host: frontHost,
});
return this.authClient.refreshAccessToken({ refreshToken }, metadata);
const data = await this.authClient.refreshAccessToken({ refreshToken }, metadata);
this.addTokenInCookie(res, {
accessToken: data.accessToken,
});
return res.send(data);
}
@ApiInternalOnlyEndpoint()
@@ -418,4 +434,93 @@ export class AuthController {
return this.authClient.resetUsers(body.users, metadata);
}
@Get('me')
async getMe(@Req() req: Request, @Res() res: Response) {
// Lê cookies
const accessToken = req.cookies['ddf-auth'];
const refreshToken = req.cookies['ddf-refresh-auth'];
console.log(req.cookies)
let payload: any;
let userInfo: any = {};
try {
// Decodifica e valida o JWT de acesso
const decoded: any = accessToken && jwt.decode(accessToken, { complete: true });
if (!decoded) throw new Error('Invalid token');
const { kid } = decoded.header;
// Busca a chave pública
const { keys } = await this.authClient.getPublicKeys();
const pemValue = keys.find((k) => k.kid === kid)?.pem;
if (!pemValue) throw new Error('Public key not found');
jwt.verify(accessToken, pemValue);
payload = decoded.payload;
userInfo = {
id: payload.user_id,
name: payload.username,
customer: {
id: payload.customer_id,
name: payload.customer_name,
tier: payload.customer_tier,
}
};
return res.status(200).json(userInfo);
} catch (err) {
// Se access token inválido, tenta refresh
if (!refreshToken) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
// Chama refreshAccessToken
const metadata = PackTheMetadata({});
const data = await this.authClient.refreshAccessToken({ refreshToken }, metadata);
// Retorna novo access token e dados mínimos
this.addTokenInCookie(res, {
accessToken: data.accessToken,
});
// Decodifica novo token
const decoded: any = jwt.decode(data.accessToken, { complete: true });
const payload = decoded.payload;
userInfo = {
id: payload.user_id,
name: payload.username,
customer: {
id: payload.customer_id,
name: payload.customer_name,
tier: payload.customer_tier,
}
};
return res.status(200).json(userInfo);
} catch (refreshErr) {
return res.status(401).json({ error: 'Not authenticated' });
}
}
}
private addTokenInCookie(res: Response, tokens: { accessToken?: string, refreshToken?: string }) {
let exp = 1000 * 60 * 5; // 5 minutes
if (tokens.accessToken) {
const { exp: expiration } = jwt.decode(tokens.accessToken) as JwtPayload;
exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms
res.cookie('ddf-auth', tokens.accessToken, {
domain: 'dadosfera.local',
maxAge: exp,
httpOnly: true,
secure: true,
sameSite: 'none', // Necessário para cookies em requisições cross-site
});
}
if (tokens.refreshToken) {
res.cookie('ddf-refresh-auth', tokens.refreshToken, {
domain: 'dadosfera.local',
maxAge: exp,
httpOnly: true,
secure: true,
sameSite: 'none', // Necessário para cookies em requisições cross-site
});
}
}
}