From 42e802f8a7400ddfa694331625869f1ff9606e0d Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Tue, 10 Feb 2026 21:45:34 -0400 Subject: [PATCH] corregido refreshtoken y mejorado ver informacion ui por roles --- .../migrations/0017_mute_mole_man.sql | 2 + .../migrations/meta/0017_snapshot.json | 2006 +++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + apps/api/src/database/schema/auth.ts | 28 +- apps/api/src/features/auth/auth.controller.ts | 25 +- apps/api/src/features/auth/auth.service.ts | 134 +- .../features/auth/dto/refresh-token.dto.ts | 4 +- apps/web/.env_template | 1 + apps/web/constants/routes.ts | 6 +- .../feactures/auth/actions/login-action.ts | 31 +- .../feactures/auth/actions/logout-action.ts | 27 + apps/web/feactures/auth/schemas/logout.ts | 5 + .../feactures/auth/schemas/refreshToken.ts | 1 - .../components/admin/surveys-header.tsx | 23 +- .../admin/surveys-tables/cell-action.tsx | 86 +- .../training-tables/cell-action.tsx | 135 +- .../training-tables/training-table-action.tsx | 21 +- apps/web/lib/auth-token.ts | 43 + apps/web/lib/auth.config.ts | 100 +- apps/web/lib/fetch.api.ts | 40 +- apps/web/lib/fetch.api2.ts | 31 +- apps/web/types/next-auth.d.ts | 6 - 22 files changed, 2438 insertions(+), 324 deletions(-) create mode 100644 apps/api/src/database/migrations/0017_mute_mole_man.sql create mode 100644 apps/api/src/database/migrations/meta/0017_snapshot.json create mode 100644 apps/web/feactures/auth/actions/logout-action.ts create mode 100644 apps/web/feactures/auth/schemas/logout.ts create mode 100644 apps/web/lib/auth-token.ts diff --git a/apps/api/src/database/migrations/0017_mute_mole_man.sql b/apps/api/src/database/migrations/0017_mute_mole_man.sql new file mode 100644 index 0000000..39c03e0 --- /dev/null +++ b/apps/api/src/database/migrations/0017_mute_mole_man.sql @@ -0,0 +1,2 @@ +ALTER TABLE "auth"."sessions" ADD COLUMN "previous_session_token" varchar;--> statement-breakpoint +ALTER TABLE "auth"."sessions" ADD COLUMN "last_rotated_at" timestamp; \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/0017_snapshot.json b/apps/api/src/database/migrations/meta/0017_snapshot.json new file mode 100644 index 0000000..90ee957 --- /dev/null +++ b/apps/api/src/database/migrations/meta/0017_snapshot.json @@ -0,0 +1,2006 @@ +{ + "id": "4afd2d9d-5f9a-4239-b3ac-0a7cd9b74fdc", + "prevId": "69862e30-9b34-4b40-b659-d30445574001", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "activityLogs_idx": { + "name": "activityLogs_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_logs_user_id_users_id_fk": { + "name": "activity_logs_user_id_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.roles": { + "name": "roles", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "roles_idx": { + "name": "roles_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_session_token": { + "name": "previous_session_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idx": { + "name": "sessions_idx", + "columns": [ + { + "expression": "session_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.users": { + "name": "users", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality": { + "name": "municipality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parish": { + "name": "parish", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_two_factor_enabled": { + "name": "is_two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_email_verified": { + "name": "is_email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_idx": { + "name": "users_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_state_states_id_fk": { + "name": "users_state_states_id_fk", + "tableFrom": "users", + "tableTo": "states", + "columnsFrom": [ + "state" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_municipality_municipalities_id_fk": { + "name": "users_municipality_municipalities_id_fk", + "tableFrom": "users", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_parish_parishes_id_fk": { + "name": "users_parish_parishes_id_fk", + "tableFrom": "users", + "tableTo": "parishes", + "columnsFrom": [ + "parish" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user_role": { + "name": "user_role", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_role_idx": { + "name": "user_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_role_user_id_users_id_fk": { + "name": "user_role_user_id_users_id_fk", + "tableFrom": "user_role", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_role_role_id_roles_id_fk": { + "name": "user_role_role_id_roles_id_fk", + "tableFrom": "user_role", + "tableTo": "roles", + "schemaTo": "auth", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verificationToken": { + "name": "verificationToken", + "schema": "auth", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.category_type": { + "name": "category_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "group": { + "name": "group", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "category_typeIx0": { + "name": "category_typeIx0", + "columns": [ + { + "expression": "group", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "category_typeIx1": { + "name": "category_typeIx1", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.localities": { + "name": "localities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parish_id": { + "name": "parish_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "localities_index_03": { + "name": "localities_index_03", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_00": { + "name": "localities_index_00", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_01": { + "name": "localities_index_01", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_02": { + "name": "localities_index_02", + "columns": [ + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "localities_state_id_states_id_fk": { + "name": "localities_state_id_states_id_fk", + "tableFrom": "localities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_municipality_id_municipalities_id_fk": { + "name": "localities_municipality_id_municipalities_id_fk", + "tableFrom": "localities", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_parish_id_parishes_id_fk": { + "name": "localities_parish_id_parishes_id_fk", + "tableFrom": "localities", + "tableTo": "parishes", + "columnsFrom": [ + "parish_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "localities_name_unique": { + "name": "localities_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipalities": { + "name": "municipalities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "municipalities_index_00": { + "name": "municipalities_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipalities_state_id_states_id_fk": { + "name": "municipalities_state_id_states_id_fk", + "tableFrom": "municipalities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parishes": { + "name": "parishes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "parishes_index_00": { + "name": "parishes_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parishes_municipality_id_municipalities_id_fk": { + "name": "parishes_municipality_id_municipalities_id_fk", + "tableFrom": "parishes", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.states": { + "name": "states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "states_index_00": { + "name": "states_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url_img": { + "name": "url_img", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gallery": { + "name": "gallery", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'BORRADOR'" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "products_user_id_users_id_fk": { + "name": "products_user_id_users_id_fk", + "tableFrom": "products", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.answers_surveys": { + "name": "answers_surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "answers": { + "name": "answers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "answers_index_00": { + "name": "answers_index_00", + "columns": [ + { + "expression": "answers", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_01": { + "name": "answers_index_01", + "columns": [ + { + "expression": "survey_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_02": { + "name": "answers_index_02", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "answers_surveys_survey_id_surveys_id_fk": { + "name": "answers_surveys_survey_id_surveys_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "surveys", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "answers_surveys_user_id_users_id_fk": { + "name": "answers_surveys_user_id_users_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.surveys": { + "name": "surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_audience": { + "name": "target_audience", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "questions": { + "name": "questions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "surveys_index_00": { + "name": "surveys_index_00", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.training_surveys": { + "name": "training_surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "firstname": { + "name": "firstname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastname": { + "name": "lastname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visit_date": { + "name": "visit_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "coor_phone": { + "name": "coor_phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality": { + "name": "municipality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parish": { + "name": "parish", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "osp_type": { + "name": "osp_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "eco_sector": { + "name": "eco_sector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "productive_sector": { + "name": "productive_sector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "central_productive_activity": { + "name": "central_productive_activity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "main_productive_activity": { + "name": "main_productive_activity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "productive_activity": { + "name": "productive_activity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_rif": { + "name": "osp_rif", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_name": { + "name": "osp_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_constitution_year": { + "name": "company_constitution_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_status": { + "name": "current_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVA'" + }, + "infrastructure_mt2": { + "name": "infrastructure_mt2", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "has_transport": { + "name": "has_transport", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "structure_type": { + "name": "structure_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_open_space": { + "name": "is_open_space", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "paralysis_reason": { + "name": "paralysis_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "equipment_list": { + "name": "equipment_list", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "production_list": { + "name": "production_list", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "product_list": { + "name": "product_list", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "osp_address": { + "name": "osp_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_google_maps_link": { + "name": "osp_google_maps_link", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "commune_name": { + "name": "commune_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "situr_code_commune": { + "name": "situr_code_commune", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commune_rif": { + "name": "commune_rif", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "commune_spokesperson_name": { + "name": "commune_spokesperson_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "commune_spokesperson_cedula": { + "name": "commune_spokesperson_cedula", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "commune_spokesperson_rif": { + "name": "commune_spokesperson_rif", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "commune_spokesperson_phone": { + "name": "commune_spokesperson_phone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "commune_email": { + "name": "commune_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "communal_council": { + "name": "communal_council", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "situr_code_communal_council": { + "name": "situr_code_communal_council", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "communal_council_rif": { + "name": "communal_council_rif", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "communal_council_spokesperson_name": { + "name": "communal_council_spokesperson_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "communal_council_spokesperson_cedula": { + "name": "communal_council_spokesperson_cedula", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "communal_council_spokesperson_rif": { + "name": "communal_council_spokesperson_rif", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "communal_council_spokesperson_phone": { + "name": "communal_council_spokesperson_phone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "communal_council_email": { + "name": "communal_council_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "osp_responsible_fullname": { + "name": "osp_responsible_fullname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_responsible_cedula": { + "name": "osp_responsible_cedula", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_responsible_rif": { + "name": "osp_responsible_rif", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "civil_state": { + "name": "civil_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_responsible_phone": { + "name": "osp_responsible_phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_responsible_email": { + "name": "osp_responsible_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family_burden": { + "name": "family_burden", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_children": { + "name": "number_of_children", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "general_observations": { + "name": "general_observations", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photo1": { + "name": "photo1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photo2": { + "name": "photo2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photo3": { + "name": "photo3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "training_surveys_index_00": { + "name": "training_surveys_index_00", + "columns": [ + { + "expression": "firstname", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "training_surveys_state_states_id_fk": { + "name": "training_surveys_state_states_id_fk", + "tableFrom": "training_surveys", + "tableTo": "states", + "columnsFrom": [ + "state" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "training_surveys_municipality_municipalities_id_fk": { + "name": "training_surveys_municipality_municipalities_id_fk", + "tableFrom": "training_surveys", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "training_surveys_parish_parishes_id_fk": { + "name": "training_surveys_parish_parishes_id_fk", + "tableFrom": "training_surveys", + "tableTo": "parishes", + "columnsFrom": [ + "parish" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "auth.gender": { + "name": "gender", + "schema": "auth", + "values": [ + "FEMENINO", + "MASCULINO" + ] + }, + "public.nationality": { + "name": "nationality", + "schema": "public", + "values": [ + "VENEZOLANO", + "EXTRANJERO" + ] + }, + "auth.status": { + "name": "status", + "schema": "auth", + "values": [ + "ACTIVE", + "INACTIVE" + ] + } + }, + "schemas": { + "auth": "auth" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "auth.user_access_view": { + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n u.id AS user_id,\n u.username,\n u.email,\n u.fullname,\n r.id AS role_id,\n r.name AS role_name\nFROM\n auth.users u\nLEFT JOIN\n auth.user_role ur ON u.id = ur.user_id \nLEFT JOIN\n auth.roles r ON ur.role_id = r.id", + "name": "user_access_view", + "schema": "auth", + "isExisting": false, + "materialized": false + }, + "public.v_product_store": { + "columns": { + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url_img": { + "name": "url_img", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gallery": { + "name": "gallery", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.gallery, p.address, p.status, p.user_id, u.fullname, u.email, u.phone\n from products p\n left join auth.users as u on u.id = p.user_id", + "name": "v_product_store", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.v_surveys": { + "columns": { + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "target_audience": { + "name": "target_audience", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select id as survey_id, title, description, created_at, closing_date, target_audience from surveys\nwhere published = true", + "name": "v_surveys", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/_journal.json b/apps/api/src/database/migrations/meta/_journal.json index 72608ff..4a2c538 100644 --- a/apps/api/src/database/migrations/meta/_journal.json +++ b/apps/api/src/database/migrations/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1769653021994, "tag": "0016_silent_tag", "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1770774052351, + "tag": "0017_mute_mole_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/database/schema/auth.ts b/apps/api/src/database/schema/auth.ts index 626050b..b81a570 100644 --- a/apps/api/src/database/schema/auth.ts +++ b/apps/api/src/database/schema/auth.ts @@ -1,9 +1,8 @@ -import * as t from 'drizzle-orm/pg-core'; import { sql } from 'drizzle-orm'; -import { authSchema } from './schemas'; +import * as t from 'drizzle-orm/pg-core'; import { timestamps } from '../timestamps'; -import { states, municipalities, parishes } from './general'; - +import { municipalities, parishes, states } from './general'; +import { authSchema } from './schemas'; // Tabla de Usuarios sistema export const users = authSchema.table( @@ -15,9 +14,15 @@ export const users = authSchema.table( fullname: t.text('fullname').notNull(), phone: t.text('phone'), password: t.text('password').notNull(), - state: t.integer('state').references(() => states.id, { onDelete: 'set null' }), - municipality: t.integer('municipality').references(() => municipalities.id, { onDelete: 'set null' }), - parish: t.integer('parish').references(() => parishes.id, { onDelete: 'set null' }), + state: t + .integer('state') + .references(() => states.id, { onDelete: 'set null' }), + municipality: t + .integer('municipality') + .references(() => municipalities.id, { onDelete: 'set null' }), + parish: t + .integer('parish') + .references(() => parishes.id, { onDelete: 'set null' }), isTwoFactorEnabled: t .boolean('is_two_factor_enabled') .notNull() @@ -32,7 +37,6 @@ export const users = authSchema.table( }), ); - // Tabla de Roles export const roles = authSchema.table( 'roles', @@ -46,8 +50,6 @@ export const roles = authSchema.table( }), ); - - //tabla User_roles export const usersRole = authSchema.table( 'user_role', @@ -88,7 +90,6 @@ LEFT JOIN LEFT JOIN auth.roles r ON ur.role_id = r.id`); - // Tabla de Sesiones export const sessions = authSchema.table( 'sessions', @@ -103,6 +104,9 @@ export const sessions = authSchema.table( .notNull(), sessionToken: t.text('session_token').notNull(), expiresAt: t.integer('expires_at').notNull(), + previousSessionToken: t.varchar('previous_session_token'), + lastRotatedAt: t.timestamp('last_rotated_at'), + ...timestamps, }, (sessions) => ({ @@ -110,8 +114,6 @@ export const sessions = authSchema.table( }), ); - - //tabla de tokens de verificación export const verificationTokens = authSchema.table( 'verificationToken', diff --git a/apps/api/src/features/auth/auth.controller.ts b/apps/api/src/features/auth/auth.controller.ts index 875fd3c..495e1d3 100644 --- a/apps/api/src/features/auth/auth.controller.ts +++ b/apps/api/src/features/auth/auth.controller.ts @@ -1,20 +1,9 @@ // api/src/feacture/auth/auth.controller.ts import { Public } from '@/common/decorators'; -import { JwtRefreshGuard } from '@/common/guards/jwt-refresh.guard'; -import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto'; import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto'; import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto'; import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto'; -import { - Body, - Controller, - Get, - HttpCode, - Patch, - Post, - Req, - UseGuards, -} from '@nestjs/common'; +import { Body, Controller, HttpCode, Patch, Post } from '@nestjs/common'; import { AuthService } from './auth.service'; @Controller('auth') @@ -58,17 +47,7 @@ export class AuthController { @Patch('refresh') //@RequirePermissions('auth:refresh-token') async refreshToken(@Body() refreshTokenDto: any) { - - console.log('refreshTokenDto', refreshTokenDto); - - const data = await this.authService.refreshToken(refreshTokenDto); - - // console.log('data', data); - - - if (!data) return null; - - return {tokens: data} + return await this.authService.refreshToken(refreshTokenDto); } // @Public() diff --git a/apps/api/src/features/auth/auth.service.ts b/apps/api/src/features/auth/auth.service.ts index 2d09d9e..7bb4670 100644 --- a/apps/api/src/features/auth/auth.service.ts +++ b/apps/api/src/features/auth/auth.service.ts @@ -27,7 +27,7 @@ import { ConfigService } from '@nestjs/config'; import { JwtService, JwtSignOptions } from '@nestjs/jwt'; import * as bcrypt from 'bcryptjs'; import crypto from 'crypto'; -import { and, eq, or } from 'drizzle-orm'; +import { eq, or } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as schema from 'src/database/index'; import { roles, sessions, users, usersRole } from 'src/database/index'; @@ -273,48 +273,116 @@ export class AuthService { //Refresh User Access Token async refreshToken(dto: RefreshTokenDto): Promise { - const secret = envs.refresh_token_secret; - const { user_id, token } = dto; + const { refreshToken } = dto; - console.log('secret', secret); - console.log('refresh_token', token); + // 1. Validar firma del token (Crypto check) + let payload: any; + try { + payload = await this.jwtService.verifyAsync(refreshToken, { + secret: envs.refresh_token_secret, + }); + } catch (e) { + throw new UnauthorizedException('Invalid Refresh Token Signature'); + } - const validation = await this.jwtService.verifyAsync(token, { - secret, - }); + const userId = Number(payload.sub); // Es más seguro usar el ID del token que el del DTO - if (!validation) throw new UnauthorizedException('Invalid refresh token'); - - const session = await this.drizzle + // 2. Buscar la sesión por UserID (SIN filtrar por token todavía) + // Esto es clave: traemos la sesión para ver qué está pasando + const [currentSession] = await this.drizzle .select() .from(sessions) - .where( - and(eq(sessions.userId, user_id), eq(sessions.sessionToken, token)), + .where(eq(sessions.userId, userId)); + + if (!currentSession) throw new NotFoundException('Session not found'); + + // CONFIGURACIÓN: Tiempo de gracia en milisegundos (ej: 15 segundos) + const GRACE_PERIOD_MS = 15000; + + // ------------------------------------------------------------------- + // ESCENARIO A: Rotación Normal (El token coincide con el actual) + // ------------------------------------------------------------------- + if (currentSession.sessionToken === refreshToken) { + const user = await this.findUserById(userId); + if (!user) throw new NotFoundException('User not found'); + + // Generar nuevos tokens (A -> B) + const tokensNew = await this.generateTokens(user); + const decodeAccess = this.decodeToken(tokensNew.access_token); + const decodeRefresh = this.decodeToken(tokensNew.refresh_token); + + // Actualizamos DB guardando el token "viejo" como "previous" + await this.drizzle + .update(sessions) + .set({ + sessionToken: tokensNew.refresh_token, // Nuevo (B) + previousSessionToken: refreshToken, // Viejo (A) + lastRotatedAt: new Date(), // Marca de tiempo + expiresAt: decodeRefresh.exp, + }) + .where(eq(sessions.userId, userId)); + + return { + access_token: tokensNew.access_token, + access_expire_in: decodeAccess.exp, + refresh_token: tokensNew.refresh_token, + refresh_expire_in: decodeRefresh.exp, + }; + } + + // ------------------------------------------------------------------- + // ESCENARIO B: Periodo de Gracia (Condición de Carrera) + // ------------------------------------------------------------------- + // El token no coincide con el actual, ¿pero coincide con el anterior? + const isPreviousToken = + currentSession.previousSessionToken === refreshToken; + + // Calculamos cuánto tiempo ha pasado desde la rotación + const timeSinceRotation = currentSession.lastRotatedAt + ? Date.now() - new Date(currentSession.lastRotatedAt).getTime() + : Infinity; + + if (isPreviousToken && timeSinceRotation < GRACE_PERIOD_MS) { + // ¡Es una condición de carrera! El usuario envió 'A' pero ya rotamos a 'B'. + // Le devolvemos 'B' (el actual en DB) para que se sincronice. + + const user = await this.findUserById(userId); + + if (!user) throw new NotFoundException('User not found'); + + // Generamos un access token nuevo fresco (barato) + const accessTokenPayload = { sub: user.id, username: user.username }; + const newAccessToken = await this.jwtService.signAsync( + accessTokenPayload, + { + secret: envs.access_token_secret, + expiresIn: envs.access_token_expiration, + } as JwtSignOptions, ); + const decodeAccess = this.decodeToken(newAccessToken); - // console.log(session.length); + // IMPORTANTE: Devolvemos el refresh token QUE YA ESTÁ EN LA BASE DE DATOS + // No generamos uno nuevo para no romper la cadena de la otra petición que ganó. + return { + access_token: newAccessToken, + access_expire_in: decodeAccess.exp, + refresh_token: currentSession.sessionToken!, // Devolvemos el token 'B' + refresh_expire_in: currentSession.expiresAt as number, + }; + } - if (session.length === 0) throw new NotFoundException('session not found'); - const user = await this.findUserById(user_id); - if (!user) throw new NotFoundException('User not found'); + // ------------------------------------------------------------------- + // ESCENARIO C: Robo de Token (Reuse Detection) + // ------------------------------------------------------------------- + // Si el token no es el actual, ni el anterior válido... ALGUIEN LO ROBÓ. + // O el usuario está intentando reusar un token muy viejo. - // Genera token - const tokens = await this.generateTokens(user); - const decodeAccess = this.decodeToken(tokens.access_token); - const decodeRefresh = this.decodeToken(tokens.refresh_token); + // Medida de seguridad: Borrar todas las sesiones del usuario + await this.drizzle.delete(sessions).where(eq(sessions.userId, userId)); - // Actualiza session - await this.drizzle - .update(sessions) - .set({ sessionToken: tokens.refresh_token, expiresAt: decodeRefresh.exp }) - .where(eq(sessions.userId, user_id)); - - return { - access_token: tokens.access_token, - access_expire_in: decodeAccess.exp, - refresh_token: tokens.refresh_token, - refresh_expire_in: decodeRefresh.exp, - }; + throw new UnauthorizedException( + 'Refresh token reuse detected. Access revoked.', + ); } async singUp(createUserDto: SingUpUserDto): Promise { diff --git a/apps/api/src/features/auth/dto/refresh-token.dto.ts b/apps/api/src/features/auth/dto/refresh-token.dto.ts index f3b72f9..fe09649 100644 --- a/apps/api/src/features/auth/dto/refresh-token.dto.ts +++ b/apps/api/src/features/auth/dto/refresh-token.dto.ts @@ -7,9 +7,9 @@ export class RefreshTokenDto { @IsString({ message: 'Refresh token must be a string', }) - token: string; + refreshToken: string; @ApiProperty() @IsNumber() - user_id: number; + userId: number; } diff --git a/apps/web/.env_template b/apps/web/.env_template index 9f3158c..5bffb2b 100644 --- a/apps/web/.env_template +++ b/apps/web/.env_template @@ -2,4 +2,5 @@ AUTH_URL = http://localhost:3000 AUTH_SECRET=wWgIwkHIGr28ydIUPsgVGNUNxXQ+brg1XXtA8PfjJjAEPJLDP2UTghWL8aE= API_URL=http://localhost:8000 NEXT_PUBLIC_API_URL=http://localhost:8000 +NODE_ENV='development' #development | production diff --git a/apps/web/constants/routes.ts b/apps/web/constants/routes.ts index 0dfd87e..c031c83 100644 --- a/apps/web/constants/routes.ts +++ b/apps/web/constants/routes.ts @@ -34,7 +34,7 @@ export const AdministrationItems: NavItem[] = [ url: '/dashboard/administracion/usuario', icon: 'userPen', shortcut: ['m', 'm'], - role: ['admin', 'superadmin', 'autoridad'], + role: ['admin', 'superadmin'], }, { title: 'Encuestas', @@ -60,7 +60,7 @@ export const StatisticsItems: NavItem[] = [ url: '#', // Placeholder as there is no direct link for the parent icon: 'chartColumn', isActive: true, - role: ['admin', 'superadmin', 'autoridad', 'manager'], + role: ['admin', 'superadmin', 'autoridad'], items: [ // { @@ -82,7 +82,7 @@ export const StatisticsItems: NavItem[] = [ shortcut: ['s', 's'], url: '/dashboard/estadisticas/socioproductiva', icon: 'blocks', - role: ['admin', 'superadmin', 'autoridad', 'manager'], + role: ['admin', 'superadmin', 'autoridad'], }, ], }, diff --git a/apps/web/feactures/auth/actions/login-action.ts b/apps/web/feactures/auth/actions/login-action.ts index 24102f6..d520ad5 100644 --- a/apps/web/feactures/auth/actions/login-action.ts +++ b/apps/web/feactures/auth/actions/login-action.ts @@ -1,5 +1,6 @@ 'use server'; import { safeFetchApi } from '@/lib'; +import { cookies } from 'next/headers'; import { loginResponseSchema, UserFormValue } from '../schemas/login'; type LoginActionSuccess = { @@ -17,18 +18,18 @@ type LoginActionSuccess = { refresh_token: string; refresh_expire_in: number; }; -} +}; type LoginActionError = { - type: 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR'; // **Asegúrate de que el tipo de `type` sea este aquí** - message: string; - details?: any; + type: 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR'; // **Asegúrate de que el tipo de `type` sea este aquí** + message: string; + details?: any; }; // Si SignInAction también puede devolver null, asegúralo en su tipo de retorno type LoginActionResult = LoginActionSuccess | LoginActionError | null; -export const SignInAction = async (payload: UserFormValue): Promise => { +export const SignInAction = async (payload: UserFormValue) => { const [error, data] = await safeFetchApi( loginResponseSchema, '/auth/sign-in', @@ -36,12 +37,22 @@ export const SignInAction = async (payload: UserFormValue): Promise { + const payload = { user_id }; + + const [error, data] = await safeFetchApi( + logoutResponseSchema, + '/auth/sign-out', + 'POST', + payload, + ); + + if (error) { + console.error('Error:', error); + // Devuelve un objeto con la propiedad 'type' para que el callback de NextAuth lo reconozca como un error + return { + type: 'API_ERROR', + message: error.message, + }; + } + + (await cookies()).delete('refresh_token'); +}; diff --git a/apps/web/feactures/auth/schemas/logout.ts b/apps/web/feactures/auth/schemas/logout.ts new file mode 100644 index 0000000..73e3f39 --- /dev/null +++ b/apps/web/feactures/auth/schemas/logout.ts @@ -0,0 +1,5 @@ +import z from 'zod'; + +export const logoutResponseSchema = z.object({ + message: z.string(), +}); diff --git a/apps/web/feactures/auth/schemas/refreshToken.ts b/apps/web/feactures/auth/schemas/refreshToken.ts index d399ed8..a71dd66 100644 --- a/apps/web/feactures/auth/schemas/refreshToken.ts +++ b/apps/web/feactures/auth/schemas/refreshToken.ts @@ -4,7 +4,6 @@ import { tokensSchema } from './login'; // Esquema para el refresh token export const refreshTokenSchema = z.object({ - user_id: z.number(), token: z.string(), }); diff --git a/apps/web/feactures/surveys/components/admin/surveys-header.tsx b/apps/web/feactures/surveys/components/admin/surveys-header.tsx index 55e3d70..4db6f9a 100644 --- a/apps/web/feactures/surveys/components/admin/surveys-header.tsx +++ b/apps/web/feactures/surveys/components/admin/surveys-header.tsx @@ -1,12 +1,14 @@ 'use client'; -import { useRouter } from 'next/navigation'; import { Button } from '@repo/shadcn/button'; import { Heading } from '@repo/shadcn/heading'; import { Plus } from 'lucide-react'; - +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; export function SurveysHeader() { const router = useRouter(); + const { data: session } = useSession(); + const role = session?.user.role[0]?.rol; return ( <>
@@ -14,11 +16,18 @@ export function SurveysHeader() { title="Administración de Encuestas" description="Gestiona las encuestas disponibles en la plataforma" /> - + {['superadmin', 'admin'].includes(role ?? '') && ( + + )}
- ); -} \ No newline at end of file +} diff --git a/apps/web/feactures/surveys/components/admin/surveys-tables/cell-action.tsx b/apps/web/feactures/surveys/components/admin/surveys-tables/cell-action.tsx index 9113c7f..e915414 100644 --- a/apps/web/feactures/surveys/components/admin/surveys-tables/cell-action.tsx +++ b/apps/web/feactures/surveys/components/admin/surveys-tables/cell-action.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; import { AlertModal } from '@/components/modal/alert-modal'; +import { useDeleteSurvey } from '@/feactures/surveys/hooks/use-mutation-surveys'; +import { SurveyTable } from '@/feactures/surveys/schemas/survey'; import { Button } from '@repo/shadcn/button'; import { Tooltip, @@ -10,9 +10,9 @@ import { TooltipTrigger, } from '@repo/shadcn/tooltip'; import { Edit, Trash } from 'lucide-react'; -import { SurveyTable } from '@/feactures/surveys/schemas/survey'; -import { useDeleteSurvey } from '@/feactures/surveys/hooks/use-mutation-surveys'; - +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; interface CellActionProps { data: SurveyTable; @@ -23,6 +23,7 @@ export const CellAction: React.FC = ({ data }) => { const [open, setOpen] = useState(false); const { mutate: deleteSurvey } = useDeleteSurvey(); const router = useRouter(); + const { data: session } = useSession(); const onConfirm = async () => { try { @@ -36,6 +37,8 @@ export const CellAction: React.FC = ({ data }) => { } }; + const role = session?.user.role[0]?.rol; + return ( <> = ({ data }) => { description="Esta acción no se puede deshacer." /> -
- - - - - - -

Editar

-
-
-
+ {['superadmin', 'admin'].includes(role ?? '') && ( + <> + + + + + + +

Editar

+
+
+
- - - - - - -

Eliminar

-
-
-
+ + + + + + +

Eliminar

+
+
+
+ + )}
); diff --git a/apps/web/feactures/training/components/training-tables/cell-action.tsx b/apps/web/feactures/training/components/training-tables/cell-action.tsx index 3fbf870..2cd7b37 100644 --- a/apps/web/feactures/training/components/training-tables/cell-action.tsx +++ b/apps/web/feactures/training/components/training-tables/cell-action.tsx @@ -9,7 +9,8 @@ import { TooltipProvider, TooltipTrigger, } from '@repo/shadcn/tooltip'; -import { Edit, Eye, Trash, FileDown } from 'lucide-react'; +import { Edit, Eye, Trash } from 'lucide-react'; +import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { TrainingViewModal } from '../training-view-modal'; @@ -25,6 +26,7 @@ export const CellAction: React.FC = ({ data, apiUrl }) => { const [viewOpen, setViewOpen] = useState(false); const { mutate: deleteTraining } = useDeleteTraining(); const router = useRouter(); + const { data: session } = useSession(); const onConfirm = async () => { try { @@ -42,6 +44,8 @@ export const CellAction: React.FC = ({ data, apiUrl }) => { window.open(`${apiUrl}/training/export/${id}`, '_blank'); }; + const role = session?.user.role[0]?.rol; + return ( <> = ({ data, apiUrl }) => { />
- - - - - - -

Ver detalle

-
-
-
+ {/* VER DETALLE: superadmin, admin, autoridad, manager */} + {['superadmin', 'admin', 'autoridad', 'manager'].includes( + role ?? '', + ) && ( + + + + + + +

Ver detalle

+
+
+
+ )} - {/* - - - - - -

Exportar Excel

-
-
-
*/} + {/* EDITAR Y ELIMINAR: Solo superadmin y admin */} + {['superadmin', 'admin'].includes(role ?? '') && ( + <> + {/* Editar */} + + + + + + +

Editar

+
+
+
- - - - - - -

Editar

-
-
-
- - - - - - - -

Eliminar

-
-
-
+ {/* Eliminar */} + + + + + + +

Eliminar

+
+
+
+ + )}
); diff --git a/apps/web/feactures/training/components/training-tables/training-table-action.tsx b/apps/web/feactures/training/components/training-tables/training-table-action.tsx index 5a170e9..b3154bd 100644 --- a/apps/web/feactures/training/components/training-tables/training-table-action.tsx +++ b/apps/web/feactures/training/components/training-tables/training-table-action.tsx @@ -3,12 +3,15 @@ import { Button } from '@repo/shadcn/components/ui/button'; import { DataTableSearch } from '@repo/shadcn/table/data-table-search'; import { Plus } from 'lucide-react'; +import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; import { useTrainingTableFilters } from './use-training-table-filters'; export default function TrainingTableAction() { const { searchQuery, setPage, setSearchQuery } = useTrainingTableFilters(); const router = useRouter(); + const { data: session } = useSession(); + const role = session?.user.role[0]?.rol; return (
@@ -19,13 +22,17 @@ export default function TrainingTableAction() { setPage={setPage} />
{' '} - + {['superadmin', 'admin', 'manager', 'coordinators'].includes( + role ?? '', + ) && ( + + )}
); } diff --git a/apps/web/lib/auth-token.ts b/apps/web/lib/auth-token.ts new file mode 100644 index 0000000..8169dd3 --- /dev/null +++ b/apps/web/lib/auth-token.ts @@ -0,0 +1,43 @@ +import { resfreshTokenAction } from '@/feactures/auth/actions/refresh-token-action'; +import { auth } from '@/lib/auth'; +import { cookies } from 'next/headers'; +import { cache } from 'react'; + +export const getValidAccessToken = cache(async () => { + const session = await auth(); + + if (!session?.access_token) return null; + + const now = Math.floor(Date.now() / 1000); + // Restamos 10s para tener margen de seguridad + const isValid = (session.access_expire_in as number) - 10 > now; + + // A. Si es válido, lo retornamos directo + if (isValid) return session.access_token; + + // B. Si expiró, buscamos la cookie + const cookieStore = cookies(); + const refreshToken = (await cookieStore).get('refresh_token')?.value; + + if (!refreshToken) return null; // No hay refresh token, fin del juego + + // C. Intentamos refrescar + const newTokens = await resfreshTokenAction({ token: refreshToken }); + + if (!newTokens) { + // Si falla el refresh (token revocado o expirado), borramos cookie + (await cookieStore).delete('refresh_token'); + return null; + } + + // D. Guardamos el nuevo refresh token en cookie y retornamos el access token + (await cookieStore).set('refresh_token', newTokens.tokens.refresh_token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 7 * 24 * 60 * 60, + }); + + return newTokens.tokens.access_token; +}); diff --git a/apps/web/lib/auth.config.ts b/apps/web/lib/auth.config.ts index 42fa1a6..83997ed 100644 --- a/apps/web/lib/auth.config.ts +++ b/apps/web/lib/auth.config.ts @@ -1,11 +1,10 @@ // lib/auth.config.ts import { SignInAction } from '@/feactures/auth/actions/login-action'; -import { resfreshTokenAction } from '@/feactures/auth/actions/refresh-token-action'; +import { logoutAction } from '@/feactures/auth/actions/logout-action'; import { CredentialsSignin, NextAuthConfig, Session, User } from 'next-auth'; -import { DefaultJWT } from 'next-auth/jwt'; +import { DefaultJWT, JWT } from 'next-auth/jwt'; import CredentialProvider from 'next-auth/providers/credentials'; - // Define los tipos para tus respuestas de SignInAction interface SignInSuccessResponse { message: string; @@ -58,8 +57,10 @@ const authConfig: NextAuthConfig = { // **NUEVO: Manejar el caso `null` primero** if (response === null) { - console.error("SignInAction returned null, indicating a potential issue before API call or generic error."); - throw new CredentialsSignin("Error de inicio de sesión inesperado."); + console.error( + 'SignInAction returned null, indicating a potential issue before API call or generic error.', + ); + throw new CredentialsSignin('Error de inicio de sesión inesperado.'); } // Tipo Guarda: Verificar la respuesta de error @@ -70,15 +71,19 @@ const authConfig: NextAuthConfig = { response.type === 'UNKNOWN_ERROR') // Incluye todos los tipos de error posibles ) { // Si es un error, lánzalo. Este camino termina aquí. - throw new CredentialsSignin("Error en la API:" + response.message); + throw new CredentialsSignin('Error en la API:' + response.message); } if (!('user' in response)) { // Esto solo ocurriría si SignInAction devolvió un objeto que no es null, // no es un error conocido por 'type', PERO tampoco tiene la propiedad 'user'. // Es un caso de respuesta inesperada del API. - console.error("Respuesta de SignInAction con formato inesperado: falta la propiedad 'user'."); - throw new CredentialsSignin("Error en el formato de la respuesta del servidor."); + console.error( + "Respuesta de SignInAction con formato inesperado: falta la propiedad 'user'.", + ); + throw new CredentialsSignin( + 'Error en el formato de la respuesta del servidor.', + ); } return { @@ -89,11 +94,7 @@ const authConfig: NextAuthConfig = { role: response?.user.rol ?? [], // Add role array access_token: response?.tokens.access_token ?? '', access_expire_in: response?.tokens.access_expire_in ?? 0, - refresh_token: response?.tokens.refresh_token ?? '', - refresh_expire_in: response?.tokens.refresh_expire_in ?? 0, }; - - }, }), ], @@ -101,11 +102,7 @@ const authConfig: NextAuthConfig = { signIn: '/', //sigin page }, callbacks: { - async jwt({ token, user }:{ - user: User - token: any - - }) { + async jwt({ token, user }: { user: User; token: any }) { // 1. Manejar el inicio de sesión inicial // El `user` solo se proporciona en el primer inicio de sesión. if (user) { @@ -117,64 +114,14 @@ const authConfig: NextAuthConfig = { role: user.role, access_token: user.access_token, access_expire_in: user.access_expire_in, - refresh_token: user.refresh_token, - refresh_expire_in: user.refresh_expire_in - } - // return token; + }; } - // 2. Si no es un nuevo login, verificar la expiración del token - const now = Math.floor(Date.now() / 1000); // Usar Math.floor para un número entero - - // Verificar si el token de acceso aún es válido - if (now < (token.access_expire_in as number)) { - return token; // Si no ha expirado, no hacer nada y devolver el token actual - } - - // console.log("Now Access Expire:",token.access_expire_in); - - - // 3. Si el token de acceso ha expirado, verificar el refresh token - // console.log("Access token ha expirado. Verificando refresh token..."); - if (now > (token.refresh_expire_in as number)) { - // console.log("Refresh token ha expirado. Forzando logout."); - return null; // Forzar el logout al devolver null - } - - // console.log("token:", token.refresh_token); - - - // 4. Si el token de acceso ha expirado pero el refresh token es válido, renovar - console.log("Renovando token de acceso..."); - try { - const refresh_token = { token: token.refresh_token as string, user_id: Number(token.id) as number} - - const res = await resfreshTokenAction(refresh_token); - - // console.log('res', res); - - - if (!res || !res.tokens) { - throw new Error('Fallo en la respuesta de la API de refresco.'); - } - - // Actualizar el token directamente con los nuevos valores - token.access_token = res.tokens.access_token; - token.access_expire_in = res.tokens.access_expire_in; - token.refresh_token = res.tokens.refresh_token; - token.refresh_expire_in = res.tokens.refresh_expire_in; - return token; - - } catch (error) { - console.error("Error al renovar el token: ", error); - return null; // Fallo al renovar, forzar logout - } + return token; }, - async session({ session, token }: { session: Session; token: any }) { + async session({ session, token }: { session: Session; token: DefaultJWT }) { session.access_token = token.access_token as string; session.access_expire_in = token.access_expire_in as number; - session.refresh_token = token.refresh_token as string; - session.refresh_expire_in = token.refresh_expire_in as number; session.user = { id: token.id as number, username: token.username as string, @@ -185,7 +132,18 @@ const authConfig: NextAuthConfig = { return session; }, }, - + events: { + async signOut(message) { + // 1. verificamos que venga token (puede no venir con algunos providers) + const token = (message as { token?: JWT }).token; + if (!token?.access_token) return; + try { + await logoutAction(String(token?.id)); + } catch { + /* silencioso para que next-auth siempre cierre */ + } + }, + }, } satisfies NextAuthConfig; export default authConfig; diff --git a/apps/web/lib/fetch.api.ts b/apps/web/lib/fetch.api.ts index e0c6583..822f741 100644 --- a/apps/web/lib/fetch.api.ts +++ b/apps/web/lib/fetch.api.ts @@ -1,6 +1,6 @@ 'use server'; import { env } from '@/lib/env'; -import axios from 'axios'; +import axios, { InternalAxiosRequestConfig } from 'axios'; import { z } from 'zod'; // Crear instancia de Axios con la URL base validada @@ -10,33 +10,21 @@ const fetchApi = axios.create({ // Interceptor para incluir el token automáticamente en las peticiones // ESTE INTERCEPTOR ESTÁ BIEN PARA EL RESTO DE LAS PETICIONES AUTENTICADAS -fetchApi.interceptors.request.use(async (config: any) => { - try { - // console.log("Solicitando autenticación..."); - - const { auth } = await import('@/lib/auth'); // Importación dinámica - const session = await auth(); - const token = session?.access_token; +fetchApi.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + try { + const { getValidAccessToken } = await import('@/lib/auth-token'); + const token = await getValidAccessToken(); - if (token) { - config.headers.Authorization = `Bearer ${token}`; + if (token) { + config.headers.set('Authorization', `Bearer ${token}`); + } + } catch (err) { + console.error('Error getting auth token:', err); } - - // **Importante:** Si el body es FormData, elimina el Content-Type para que Axios lo configure automáticamente. - if (config.data instanceof FormData) { - delete config.headers['Content-Type']; - } else { - config.headers['Content-Type'] = 'application/json'; - } - return config; - } catch (error) { - console.error('Error al obtener el token de autenticación para el interceptor:', error); - // IMPORTANTE: Si ocurre un error aquí, es mejor rechazar la promesa - // para que la solicitud no se envíe sin autorización. - return Promise.reject(error); - } -}); + }, +); // safeFetchApi sigue siendo útil para el resto de las llamadas que requieren autenticación export const safeFetchApi = async >( @@ -97,4 +85,4 @@ export const safeFetchApi = async >( } }; -export { fetchApi }; \ No newline at end of file +export { fetchApi }; diff --git a/apps/web/lib/fetch.api2.ts b/apps/web/lib/fetch.api2.ts index 0f9a739..9dfd4ca 100644 --- a/apps/web/lib/fetch.api2.ts +++ b/apps/web/lib/fetch.api2.ts @@ -1,6 +1,6 @@ 'use server'; import { env } from '@/lib/env'; // Importamos la configuración de entorno validada -import axios from 'axios'; +import axios, { InternalAxiosRequestConfig } from 'axios'; import { z } from 'zod'; // Crear instancia de Axios con la URL base validada @@ -12,22 +12,21 @@ const fetchApi = axios.create({ }); // Interceptor para incluir el token automáticamente en las peticiones -fetchApi.interceptors.request.use(async (config: any) => { - try { - // Importación dinámica para evitar la referencia circular - const { auth } = await import('@/lib/auth'); - const session = await auth(); - const token = session?.access_token; +fetchApi.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + try { + const { getValidAccessToken } = await import('@/lib/auth-token'); + const token = await getValidAccessToken(); - if (token) { - config.headers.Authorization = `Bearer ${token}`; + if (token) { + config.headers.set('Authorization', `Bearer ${token}`); + } + } catch (err) { + console.error('Error getting auth token:', err); } - } catch (error) { - console.error('Error getting auth token:', error); - } - - return config; -}); + return config; + }, +); /** * Función para hacer peticiones con validación de respuesta @@ -96,4 +95,4 @@ export const safeFetchApi = async >( } }; -export { fetchApi }; \ No newline at end of file +export { fetchApi }; diff --git a/apps/web/types/next-auth.d.ts b/apps/web/types/next-auth.d.ts index e900a24..bc6a517 100644 --- a/apps/web/types/next-auth.d.ts +++ b/apps/web/types/next-auth.d.ts @@ -4,8 +4,6 @@ declare module 'next-auth' { interface Session extends DefaultSession { access_token: string; access_expire_in: number; - refresh_token: string; - refresh_expire_in: number; user: { id: number; username: string; @@ -29,8 +27,6 @@ declare module 'next-auth' { }>; access_token: string; access_expire_in: number; - refresh_token: string; - refresh_expire_in: number; } } @@ -46,7 +42,5 @@ declare module 'next-auth/jwt' { }>; access_token: string; access_expire_in: number; - refresh_token: string; - refresh_expire_in: number; } }