ar-saas 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/package.json +2 -1
  2. package/templates/backend/.env.example +13 -3
  3. package/templates/backend/README.md +22 -3
  4. package/templates/backend/package-lock.json +165 -2
  5. package/templates/backend/package.json +2 -0
  6. package/templates/backend/src/app.module.ts +14 -0
  7. package/templates/backend/src/common/guards/github-auth.guard.ts +5 -0
  8. package/templates/backend/src/main.ts +2 -2
  9. package/templates/backend/src/modules/auth/auth.controller.ts +51 -3
  10. package/templates/backend/src/modules/auth/auth.module.ts +2 -1
  11. package/templates/backend/src/modules/auth/auth.service.ts +96 -11
  12. package/templates/backend/src/modules/auth/strategies/github.strategy.ts +46 -0
  13. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +1 -1
  14. package/templates/backend/src/modules/clients/clients.controller.ts +91 -0
  15. package/templates/backend/src/modules/clients/clients.module.ts +16 -0
  16. package/templates/backend/src/modules/clients/clients.repository.ts +14 -0
  17. package/templates/backend/src/modules/clients/clients.service.ts +52 -0
  18. package/templates/backend/src/modules/clients/dto/create-client.dto.ts +40 -0
  19. package/templates/backend/src/modules/clients/dto/query-client.dto.ts +30 -0
  20. package/templates/backend/src/modules/clients/dto/update-client.dto.ts +4 -0
  21. package/templates/backend/src/modules/clients/schemas/client.schema.ts +32 -0
  22. package/templates/backend/src/modules/invoices/dto/create-invoice.dto.ts +79 -0
  23. package/templates/backend/src/modules/invoices/dto/invoice-item.dto.ts +23 -0
  24. package/templates/backend/src/modules/invoices/dto/query-invoice.dto.ts +40 -0
  25. package/templates/backend/src/modules/invoices/dto/update-invoice.dto.ts +4 -0
  26. package/templates/backend/src/modules/invoices/invoices.controller.ts +91 -0
  27. package/templates/backend/src/modules/invoices/invoices.module.ts +18 -0
  28. package/templates/backend/src/modules/invoices/invoices.repository.ts +14 -0
  29. package/templates/backend/src/modules/invoices/invoices.service.ts +104 -0
  30. package/templates/backend/src/modules/invoices/schemas/invoice.schema.ts +75 -0
  31. package/templates/backend/src/modules/notifications/dto/create-notification.dto.ts +45 -0
  32. package/templates/backend/src/modules/notifications/dto/query-notification.dto.ts +30 -0
  33. package/templates/backend/src/modules/notifications/dto/update-notification.dto.ts +4 -0
  34. package/templates/backend/src/modules/notifications/notifications.controller.ts +119 -0
  35. package/templates/backend/src/modules/notifications/notifications.module.ts +16 -0
  36. package/templates/backend/src/modules/notifications/notifications.repository.ts +31 -0
  37. package/templates/backend/src/modules/notifications/notifications.service.ts +64 -0
  38. package/templates/backend/src/modules/notifications/schemas/notification.schema.ts +38 -0
  39. package/templates/backend/src/modules/pipeline/dto/create-deal.dto.ts +40 -0
  40. package/templates/backend/src/modules/pipeline/dto/query-deal.dto.ts +35 -0
  41. package/templates/backend/src/modules/pipeline/dto/update-deal.dto.ts +4 -0
  42. package/templates/backend/src/modules/pipeline/pipeline.controller.ts +91 -0
  43. package/templates/backend/src/modules/pipeline/pipeline.module.ts +18 -0
  44. package/templates/backend/src/modules/pipeline/pipeline.repository.ts +14 -0
  45. package/templates/backend/src/modules/pipeline/pipeline.service.ts +64 -0
  46. package/templates/backend/src/modules/pipeline/schemas/deal.schema.ts +39 -0
  47. package/templates/backend/src/modules/planner/dto/create-planner-block.dto.ts +66 -0
  48. package/templates/backend/src/modules/planner/dto/query-planner-block.dto.ts +48 -0
  49. package/templates/backend/src/modules/planner/dto/update-block-status.dto.ts +10 -0
  50. package/templates/backend/src/modules/planner/dto/update-planner-block.dto.ts +4 -0
  51. package/templates/backend/src/modules/planner/planner.controller.ts +124 -0
  52. package/templates/backend/src/modules/planner/planner.module.ts +16 -0
  53. package/templates/backend/src/modules/planner/planner.repository.ts +45 -0
  54. package/templates/backend/src/modules/planner/planner.service.ts +104 -0
  55. package/templates/backend/src/modules/planner/schemas/planner-block.schema.ts +56 -0
  56. package/templates/backend/src/modules/task-columns/dto/create-task-column.dto.ts +20 -0
  57. package/templates/backend/src/modules/task-columns/dto/reorder-columns.dto.ts +9 -0
  58. package/templates/backend/src/modules/task-columns/dto/update-task-column.dto.ts +4 -0
  59. package/templates/backend/src/modules/task-columns/schemas/task-column.schema.ts +21 -0
  60. package/templates/backend/src/modules/task-columns/task-columns.controller.ts +86 -0
  61. package/templates/backend/src/modules/task-columns/task-columns.module.ts +16 -0
  62. package/templates/backend/src/modules/task-columns/task-columns.repository.ts +15 -0
  63. package/templates/backend/src/modules/task-columns/task-columns.service.ts +49 -0
  64. package/templates/backend/src/modules/tasks/dto/checklist-item.dto.ts +13 -0
  65. package/templates/backend/src/modules/tasks/dto/create-task.dto.ts +67 -0
  66. package/templates/backend/src/modules/tasks/dto/label.dto.ts +12 -0
  67. package/templates/backend/src/modules/tasks/dto/move-task.dto.ts +15 -0
  68. package/templates/backend/src/modules/tasks/dto/query-task.dto.ts +40 -0
  69. package/templates/backend/src/modules/tasks/dto/update-task.dto.ts +4 -0
  70. package/templates/backend/src/modules/tasks/schemas/task.schema.ts +66 -0
  71. package/templates/backend/src/modules/tasks/tasks.controller.ts +104 -0
  72. package/templates/backend/src/modules/tasks/tasks.module.ts +18 -0
  73. package/templates/backend/src/modules/tasks/tasks.repository.ts +14 -0
  74. package/templates/backend/src/modules/tasks/tasks.service.ts +76 -0
  75. package/templates/backend/src/modules/users/schemas/user.schema.ts +3 -0
  76. package/templates/backend/src/modules/users/users.repository.ts +8 -0
  77. package/templates/backend/src/modules/users/users.service.ts +34 -0
  78. package/templates/frontend/.env.local.example +1 -1
  79. package/templates/frontend/README.md +43 -1
  80. package/templates/frontend/package.json +48 -45
  81. package/templates/frontend/pnpm-lock.yaml +5096 -5012
  82. package/templates/frontend/src/app/(auth)/layout.tsx +7 -1
  83. package/templates/frontend/src/app/(auth)/login/page.tsx +13 -0
  84. package/templates/frontend/src/app/(auth)/register/page.tsx +13 -0
  85. package/templates/frontend/src/app/(dashboard)/clients/page.tsx +295 -0
  86. package/templates/frontend/src/app/(dashboard)/invoices/page.tsx +305 -0
  87. package/templates/frontend/src/app/(dashboard)/notifications/page.tsx +173 -0
  88. package/templates/frontend/src/app/(dashboard)/pipeline/page.tsx +244 -0
  89. package/templates/frontend/src/app/(dashboard)/planner/page.tsx +287 -0
  90. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +165 -128
  91. package/templates/frontend/src/app/(dashboard)/tasks/page.tsx +366 -0
  92. package/templates/frontend/src/app/auth/github/callback/page.tsx +82 -0
  93. package/templates/frontend/src/app/landing/page.tsx +21 -0
  94. package/templates/frontend/src/app/page.tsx +5 -5
  95. package/templates/frontend/src/app/setup/page.tsx +15 -14
  96. package/templates/frontend/src/components/auth/github-button.tsx +25 -0
  97. package/templates/frontend/src/components/dashboard/sidebar.tsx +90 -71
  98. package/templates/frontend/src/components/ui/alert-dialog.tsx +141 -0
  99. package/templates/frontend/src/components/ui/button.tsx +56 -52
  100. package/templates/frontend/src/components/ui/popover.tsx +31 -0
  101. package/templates/frontend/src/components/ui/select.tsx +160 -0
  102. package/templates/frontend/src/components/ui/sheet.tsx +140 -0
  103. package/templates/frontend/src/lib/api/auth.ts +7 -0
  104. package/templates/frontend/src/lib/api/clients.ts +17 -0
  105. package/templates/frontend/src/lib/api/invoices.ts +18 -0
  106. package/templates/frontend/src/lib/api/notifications.ts +27 -0
  107. package/templates/frontend/src/lib/api/pipeline.ts +18 -0
  108. package/templates/frontend/src/lib/api/planner.ts +26 -0
  109. package/templates/frontend/src/lib/api/task-columns.ts +17 -0
  110. package/templates/frontend/src/lib/api/tasks.ts +21 -0
  111. package/templates/frontend/src/lib/hooks/use-unread-notifications.ts +23 -0
  112. package/templates/frontend/src/providers/auth-provider.tsx +7 -1
  113. package/templates/frontend/src/types/clients.ts +38 -0
  114. package/templates/frontend/src/types/invoices.ts +51 -0
  115. package/templates/frontend/src/types/notifications.ts +30 -0
  116. package/templates/frontend/src/types/pipeline.ts +35 -0
  117. package/templates/frontend/src/types/planner.ts +49 -0
  118. package/templates/frontend/src/types/tasks.ts +65 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ar-saas",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Generador de proyectos SaaS multi-tenant para startups argentinas. Landing page, auth, dashboard y legal listos para producción.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -51,3 +51,4 @@
51
51
  }
52
52
  }
53
53
 
54
+
@@ -10,7 +10,7 @@
10
10
  NODE_ENV=development
11
11
 
12
12
  # Puerto donde corre el servidor
13
- PORT=3000
13
+ PORT=3001
14
14
 
15
15
  # Prefijo para todas las rutas de la API
16
16
  API_PREFIX=api
@@ -47,7 +47,7 @@ RESEND_FROM_NAME="Tu App"
47
47
 
48
48
  # ===== App URL =====
49
49
  # URL del frontend (para links en emails de verificación, reset, etc)
50
- APP_URL=http://localhost:5173
50
+ APP_URL=http://localhost:3000
51
51
 
52
52
  # ===== Swagger =====
53
53
  # Habilitar documentación Swagger en /api/docs (solo development)
@@ -55,7 +55,7 @@ SWAGGER_ENABLED=true
55
55
 
56
56
  # ===== CORS =====
57
57
  # Orígenes permitidos (separados por coma)
58
- CORS_ORIGINS=http://localhost:3001
58
+ CORS_ORIGINS=http://localhost:3000
59
59
 
60
60
  # ===== Rate Limiting =====
61
61
  # Cantidad máxima de requests por ventana de tiempo
@@ -65,3 +65,13 @@ THROTTLE_LIMIT=100
65
65
  # ===== Cookies =====
66
66
  # Secreto para firmar cookies (usar openssl rand -hex 32 para generar)
67
67
  COOKIE_SECRET=cambiar-por-secreto-seguro
68
+
69
+ # ===== GitHub OAuth =====
70
+ # Crear en https://github.com/settings/applications/new
71
+ # Authorization callback URL: http://localhost:3001/api/auth/github/callback
72
+ GITHUB_CLIENT_ID=cambiar-por-client-id
73
+ GITHUB_CLIENT_SECRET=cambiar-por-client-secret
74
+ GITHUB_CALLBACK_URL=http://localhost:3001/api/auth/github/callback
75
+
76
+ # URL del frontend (para redirigir después del OAuth)
77
+ FRONTEND_URL=http://localhost:3000
@@ -39,7 +39,7 @@ todo lo genérico ya resuelto, más integraciones reales para operar en Argentin
39
39
  | ODM | Mongoose | 9 |
40
40
  | Auth (tokens) | JWT + cookies HttpOnly | — |
41
41
  | Auth (passwords) | bcrypt | 5.x |
42
- | Auth (passport) | passport + passport-jwt | 0.7 / 4.x |
42
+ | Auth (passport) | passport + passport-jwt + passport-github2 | 0.7 / 4.x / 0.1.x |
43
43
  | Validación | class-validator | 0.14 |
44
44
  | Transformación | class-transformer | 0.5 |
45
45
  | Emails | Resend | 4.x |
@@ -53,7 +53,7 @@ todo lo genérico ya resuelto, más integraciones reales para operar en Argentin
53
53
 
54
54
  - **Multi-tenancy real** — Aislamiento por `workspaceId` garantizado en cada query. El `workspaceId` se extrae del JWT verificado del usuario autenticado. `BaseRepository` fuerza el filtro en toda operación. Imposible leakear datos entre workspaces por error humano.
55
55
 
56
- - **Auth completo** — Registro, login, refresh token rotativo, email verification, password reset. Access + refresh tokens en cookies `HttpOnly`, `Secure`, `SameSite=Strict`. Con rate limiting en endpoints sensibles y Helmet para headers de seguridad. Nunca en `localStorage` ni en el body.
56
+ - **Auth completo** — Registro, login, refresh token rotativo, email verification, password reset y **OAuth con GitHub**. Access + refresh tokens en cookies `HttpOnly`, `Secure`. `SameSite=none` + `Partitioned` en producción para soporte cross-domain. Con rate limiting en endpoints sensibles y Helmet para headers de seguridad. Nunca en `localStorage` ni en el body.
57
57
 
58
58
  - **BaseRepository genérico** — Soft delete, paginación, filtros dinámicos, agregaciones, conteo, upsert. Manejo automático de errores MongoDB (duplicate key, cast error). 12 métodos heredados por todos los repositorios.
59
59
 
@@ -124,6 +124,23 @@ Instalá MongoDB Community Edition siguiendo las instrucciones oficiales:
124
124
  | `RESEND_FROM_EMAIL` | Email remitente verificado en Resend |
125
125
  | `APP_URL` | URL del frontend: `http://localhost:3001` |
126
126
  | `CORS_ORIGINS` | URL del frontend (misma que APP_URL): `http://localhost:3001` |
127
+ | `GITHUB_CLIENT_ID` | Client ID de la GitHub OAuth App |
128
+ | `GITHUB_CLIENT_SECRET` | Client Secret de la GitHub OAuth App |
129
+ | `GITHUB_CALLBACK_URL` | `http://localhost:3001/api/auth/github/callback` en desarrollo |
130
+ | `FRONTEND_URL` | URL del frontend para redirigir después del OAuth: `http://localhost:3000` |
131
+
132
+ **Configurar GitHub OAuth:**
133
+
134
+ 1. Ir a [github.com/settings/applications/new](https://github.com/settings/applications/new)
135
+ 2. Completar:
136
+ - **Application name**: nombre de tu app
137
+ - **Homepage URL**: `http://localhost:3000`
138
+ - **Authorization callback URL**: `http://localhost:3001/api/auth/github/callback`
139
+ 3. Hacer click en **Register application**
140
+ 4. Copiar el **Client ID** → `GITHUB_CLIENT_ID`
141
+ 5. Generar un **Client Secret** → `GITHUB_CLIENT_SECRET`
142
+
143
+ En producción, crear una segunda OAuth App con las URLs de producción, o actualizar la existente.
127
144
 
128
145
  **Generar JWT secrets:**
129
146
 
@@ -167,6 +184,8 @@ El servidor corre en `http://localhost:3000/api`. Swagger en `http://localhost:3
167
184
  | `secretOrPrivateKey must have a value` | `JWT_ACCESS_SECRET` o `JWT_REFRESH_SECRET` vacíos en `.env` |
168
185
  | Error de CORS en el frontend | `CORS_ORIGINS` en `.env` no incluye `http://localhost:3001` |
169
186
  | Emails no llegan | Verificar `RESEND_API_KEY` y que `RESEND_FROM_EMAIL` esté verificado en Resend |
187
+ | GitHub OAuth: `redirect_uri_mismatch` | La callback URL en la GitHub App no coincide con `GITHUB_CALLBACK_URL` en `.env` |
188
+ | GitHub OAuth: `No public email on GitHub account` | El usuario de GitHub tiene el email en privado — debe hacerlo público en [github.com/settings/profile](https://github.com/settings/profile) |
170
189
 
171
190
  ## Comandos disponibles
172
191
 
@@ -199,7 +218,7 @@ src/
199
218
  │ └── interceptors/
200
219
  │ └── workspace-tenant.interceptor.ts # Extrae workspaceId del JWT verificado
201
220
  ├── modules/
202
- │ ├── auth/ # Registro, login, refresh, email verification, password reset
221
+ │ ├── auth/ # Registro, login, refresh, email verification, password reset, GitHub OAuth
203
222
  │ ├── users/ # CRUD de usuarios
204
223
  │ ├── workspaces/ # CRUD de workspaces
205
224
  │ └── mail/ # Envío de emails con Resend (@Global)
@@ -1,11 +1,11 @@
1
1
  {
2
- "name": "create-saas-ar-backend",
2
+ "name": "ar-saas-backend",
3
3
  "version": "0.0.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
- "name": "create-saas-ar-backend",
8
+ "name": "ar-saas-backend",
9
9
  "version": "0.0.1",
10
10
  "license": "UNLICENSED",
11
11
  "dependencies": {
@@ -18,12 +18,16 @@
18
18
  "@nestjs/platform-express": "^11.0.1",
19
19
  "@nestjs/schedule": "^6.0.1",
20
20
  "@nestjs/swagger": "^11.2.1",
21
+ "@nestjs/throttler": "^6.0.0",
21
22
  "bcryptjs": "^2.4.3",
22
23
  "class-transformer": "^0.5.1",
23
24
  "class-validator": "^0.14.2",
24
25
  "cookie-parser": "^1.4.7",
26
+ "helmet": "^8.0.0",
27
+ "joi": "^17.13.3",
25
28
  "mongoose": "^9.0.1",
26
29
  "passport": "^0.7.0",
30
+ "passport-github2": "^0.1.12",
27
31
  "passport-jwt": "^4.0.1",
28
32
  "reflect-metadata": "^0.2.2",
29
33
  "resend": "^4.1.2",
@@ -40,6 +44,7 @@
40
44
  "@types/express": "^5.0.0",
41
45
  "@types/jest": "^30.0.0",
42
46
  "@types/node": "^22.10.7",
47
+ "@types/passport-github2": "^1.2.9",
43
48
  "@types/passport-jwt": "^4.0.1",
44
49
  "@types/supertest": "^6.0.2",
45
50
  "eslint": "^9.18.0",
@@ -954,6 +959,21 @@
954
959
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
955
960
  }
956
961
  },
962
+ "node_modules/@hapi/hoek": {
963
+ "version": "9.3.0",
964
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
965
+ "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
966
+ "license": "BSD-3-Clause"
967
+ },
968
+ "node_modules/@hapi/topo": {
969
+ "version": "5.1.0",
970
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
971
+ "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
972
+ "license": "BSD-3-Clause",
973
+ "dependencies": {
974
+ "@hapi/hoek": "^9.0.0"
975
+ }
976
+ },
957
977
  "node_modules/@humanfs/core": {
958
978
  "version": "0.19.2",
959
979
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -2595,6 +2615,17 @@
2595
2615
  }
2596
2616
  }
2597
2617
  },
2618
+ "node_modules/@nestjs/throttler": {
2619
+ "version": "6.5.0",
2620
+ "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz",
2621
+ "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==",
2622
+ "license": "MIT",
2623
+ "peerDependencies": {
2624
+ "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
2625
+ "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
2626
+ "reflect-metadata": "^0.1.13 || ^0.2.0"
2627
+ }
2628
+ },
2598
2629
  "node_modules/@noble/hashes": {
2599
2630
  "version": "1.8.0",
2600
2631
  "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@@ -2696,6 +2727,27 @@
2696
2727
  "url": "https://ko-fi.com/killymxi"
2697
2728
  }
2698
2729
  },
2730
+ "node_modules/@sideway/address": {
2731
+ "version": "4.1.5",
2732
+ "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
2733
+ "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
2734
+ "license": "BSD-3-Clause",
2735
+ "dependencies": {
2736
+ "@hapi/hoek": "^9.0.0"
2737
+ }
2738
+ },
2739
+ "node_modules/@sideway/formula": {
2740
+ "version": "3.0.1",
2741
+ "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
2742
+ "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
2743
+ "license": "BSD-3-Clause"
2744
+ },
2745
+ "node_modules/@sideway/pinpoint": {
2746
+ "version": "2.0.0",
2747
+ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
2748
+ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
2749
+ "license": "BSD-3-Clause"
2750
+ },
2699
2751
  "node_modules/@sinclair/typebox": {
2700
2752
  "version": "0.34.49",
2701
2753
  "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz",
@@ -3019,6 +3071,16 @@
3019
3071
  "undici-types": "~6.21.0"
3020
3072
  }
3021
3073
  },
3074
+ "node_modules/@types/oauth": {
3075
+ "version": "0.9.6",
3076
+ "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz",
3077
+ "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==",
3078
+ "dev": true,
3079
+ "license": "MIT",
3080
+ "dependencies": {
3081
+ "@types/node": "*"
3082
+ }
3083
+ },
3022
3084
  "node_modules/@types/passport": {
3023
3085
  "version": "1.0.17",
3024
3086
  "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
@@ -3029,6 +3091,18 @@
3029
3091
  "@types/express": "*"
3030
3092
  }
3031
3093
  },
3094
+ "node_modules/@types/passport-github2": {
3095
+ "version": "1.2.9",
3096
+ "resolved": "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.2.9.tgz",
3097
+ "integrity": "sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==",
3098
+ "dev": true,
3099
+ "license": "MIT",
3100
+ "dependencies": {
3101
+ "@types/express": "*",
3102
+ "@types/passport": "*",
3103
+ "@types/passport-oauth2": "*"
3104
+ }
3105
+ },
3032
3106
  "node_modules/@types/passport-jwt": {
3033
3107
  "version": "4.0.1",
3034
3108
  "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
@@ -3040,6 +3114,18 @@
3040
3114
  "@types/passport-strategy": "*"
3041
3115
  }
3042
3116
  },
3117
+ "node_modules/@types/passport-oauth2": {
3118
+ "version": "1.8.0",
3119
+ "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
3120
+ "integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==",
3121
+ "dev": true,
3122
+ "license": "MIT",
3123
+ "dependencies": {
3124
+ "@types/express": "*",
3125
+ "@types/oauth": "*",
3126
+ "@types/passport": "*"
3127
+ }
3128
+ },
3043
3129
  "node_modules/@types/passport-strategy": {
3044
3130
  "version": "0.2.38",
3045
3131
  "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
@@ -4319,6 +4405,15 @@
4319
4405
  ],
4320
4406
  "license": "MIT"
4321
4407
  },
4408
+ "node_modules/base64url": {
4409
+ "version": "3.0.1",
4410
+ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
4411
+ "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
4412
+ "license": "MIT",
4413
+ "engines": {
4414
+ "node": ">=6.0.0"
4415
+ }
4416
+ },
4322
4417
  "node_modules/baseline-browser-mapping": {
4323
4418
  "version": "2.10.34",
4324
4419
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz",
@@ -6416,6 +6511,18 @@
6416
6511
  "node": ">= 0.4"
6417
6512
  }
6418
6513
  },
6514
+ "node_modules/helmet": {
6515
+ "version": "8.2.0",
6516
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz",
6517
+ "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==",
6518
+ "license": "MIT",
6519
+ "engines": {
6520
+ "node": ">=18.0.0"
6521
+ },
6522
+ "funding": {
6523
+ "url": "https://github.com/sponsors/EvanHahn"
6524
+ }
6525
+ },
6419
6526
  "node_modules/html-escaper": {
6420
6527
  "version": "2.0.2",
6421
6528
  "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -7558,6 +7665,19 @@
7558
7665
  "url": "https://github.com/chalk/supports-color?sponsor=1"
7559
7666
  }
7560
7667
  },
7668
+ "node_modules/joi": {
7669
+ "version": "17.13.3",
7670
+ "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
7671
+ "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
7672
+ "license": "BSD-3-Clause",
7673
+ "dependencies": {
7674
+ "@hapi/hoek": "^9.3.0",
7675
+ "@hapi/topo": "^5.1.0",
7676
+ "@sideway/address": "^4.1.5",
7677
+ "@sideway/formula": "^3.0.1",
7678
+ "@sideway/pinpoint": "^2.0.0"
7679
+ }
7680
+ },
7561
7681
  "node_modules/js-tokens": {
7562
7682
  "version": "4.0.0",
7563
7683
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8406,6 +8526,12 @@
8406
8526
  "node": ">=8"
8407
8527
  }
8408
8528
  },
8529
+ "node_modules/oauth": {
8530
+ "version": "0.10.2",
8531
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
8532
+ "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
8533
+ "license": "MIT"
8534
+ },
8409
8535
  "node_modules/object-assign": {
8410
8536
  "version": "4.1.1",
8411
8537
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -8627,6 +8753,17 @@
8627
8753
  "url": "https://github.com/sponsors/jaredhanson"
8628
8754
  }
8629
8755
  },
8756
+ "node_modules/passport-github2": {
8757
+ "version": "0.1.12",
8758
+ "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz",
8759
+ "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==",
8760
+ "dependencies": {
8761
+ "passport-oauth2": "1.x.x"
8762
+ },
8763
+ "engines": {
8764
+ "node": ">= 0.8.0"
8765
+ }
8766
+ },
8630
8767
  "node_modules/passport-jwt": {
8631
8768
  "version": "4.0.1",
8632
8769
  "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
@@ -8637,6 +8774,26 @@
8637
8774
  "passport-strategy": "^1.0.0"
8638
8775
  }
8639
8776
  },
8777
+ "node_modules/passport-oauth2": {
8778
+ "version": "1.8.0",
8779
+ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
8780
+ "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
8781
+ "license": "MIT",
8782
+ "dependencies": {
8783
+ "base64url": "3.x.x",
8784
+ "oauth": "0.10.x",
8785
+ "passport-strategy": "1.x.x",
8786
+ "uid2": "0.0.x",
8787
+ "utils-merge": "1.x.x"
8788
+ },
8789
+ "engines": {
8790
+ "node": ">= 0.4.0"
8791
+ },
8792
+ "funding": {
8793
+ "type": "github",
8794
+ "url": "https://github.com/sponsors/jaredhanson"
8795
+ }
8796
+ },
8640
8797
  "node_modules/passport-strategy": {
8641
8798
  "version": "1.0.0",
8642
8799
  "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
@@ -10405,6 +10562,12 @@
10405
10562
  "node": ">=8"
10406
10563
  }
10407
10564
  },
10565
+ "node_modules/uid2": {
10566
+ "version": "0.0.4",
10567
+ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
10568
+ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
10569
+ "license": "MIT"
10570
+ },
10408
10571
  "node_modules/uint8array-extras": {
10409
10572
  "version": "1.5.0",
10410
10573
  "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
@@ -38,6 +38,7 @@
38
38
  "joi": "^17.13.3",
39
39
  "mongoose": "^9.0.1",
40
40
  "passport": "^0.7.0",
41
+ "passport-github2": "^0.1.12",
41
42
  "passport-jwt": "^4.0.1",
42
43
  "reflect-metadata": "^0.2.2",
43
44
  "resend": "^4.1.2",
@@ -54,6 +55,7 @@
54
55
  "@types/express": "^5.0.0",
55
56
  "@types/jest": "^30.0.0",
56
57
  "@types/node": "^22.10.7",
58
+ "@types/passport-github2": "^1.2.9",
57
59
  "@types/passport-jwt": "^4.0.1",
58
60
  "@types/supertest": "^6.0.2",
59
61
  "eslint": "^9.18.0",
@@ -8,7 +8,14 @@ import { AppController } from './app.controller';
8
8
  import { AppService } from './app.service';
9
9
  import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
10
10
  import { AuthModule } from './modules/auth/auth.module';
11
+ import { ClientsModule } from './modules/clients/clients.module';
12
+ import { InvoicesModule } from './modules/invoices/invoices.module';
11
13
  import { MailModule } from './modules/mail/mail.module';
14
+ import { NotificationsModule } from './modules/notifications/notifications.module';
15
+ import { PipelineModule } from './modules/pipeline/pipeline.module';
16
+ import { PlannerModule } from './modules/planner/planner.module';
17
+ import { TaskColumnsModule } from './modules/task-columns/task-columns.module';
18
+ import { TasksModule } from './modules/tasks/tasks.module';
12
19
  import { UsersModule } from './modules/users/users.module';
13
20
  import { WorkspacesModule } from './modules/workspaces/workspaces.module';
14
21
 
@@ -51,6 +58,13 @@ import { WorkspacesModule } from './modules/workspaces/workspaces.module';
51
58
  UsersModule,
52
59
  WorkspacesModule,
53
60
  AuthModule,
61
+ ClientsModule,
62
+ NotificationsModule,
63
+ InvoicesModule,
64
+ PipelineModule,
65
+ TaskColumnsModule,
66
+ TasksModule,
67
+ PlannerModule,
54
68
  ],
55
69
  controllers: [AppController],
56
70
  providers: [
@@ -0,0 +1,5 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AuthGuard } from '@nestjs/passport';
3
+
4
+ @Injectable()
5
+ export class GithubAuthGuard extends AuthGuard('github') {}
@@ -15,7 +15,7 @@ async function bootstrap() {
15
15
  app.use(cookieParser(process.env.COOKIE_SECRET));
16
16
 
17
17
  app.enableCors({
18
- origin: process.env.CORS_ORIGINS?.split(',') ?? ['http://localhost:5173'],
18
+ origin: process.env.CORS_ORIGINS?.split(',') ?? ['http://localhost:3000'],
19
19
  credentials: true,
20
20
  });
21
21
 
@@ -42,7 +42,7 @@ async function bootstrap() {
42
42
  SwaggerModule.setup(`${apiPrefix}/docs`, app, document);
43
43
  }
44
44
 
45
- const port = process.env.PORT ?? 3000;
45
+ const port = process.env.PORT ?? 3001;
46
46
  await app.listen(port);
47
47
  console.log(`🚀 Servidor corriendo en http://localhost:${port}/${apiPrefix}`);
48
48
  }
@@ -6,6 +6,7 @@ import {
6
6
  HttpStatus,
7
7
  Post,
8
8
  Query,
9
+ Req,
9
10
  Res,
10
11
  UnauthorizedException,
11
12
  UseGuards,
@@ -13,11 +14,12 @@ import {
13
14
  import { ConfigService } from '@nestjs/config';
14
15
  import { Throttle } from '@nestjs/throttler';
15
16
  import { ApiOperation, ApiTags } from '@nestjs/swagger';
16
- import type { Response } from 'express';
17
+ import type { Request, Response } from 'express';
17
18
  import { CurrentUser } from '../../common/decorators/current-user.decorator';
18
19
  import type { TokenPayload } from '../../common/decorators/current-user.decorator';
19
20
  import { Cookie } from '../../common/decorators/cookie.decorator';
20
21
  import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
22
+ import { GithubAuthGuard } from '../../common/guards/github-auth.guard';
21
23
  import { AuthService } from './auth.service';
22
24
  import { ForgotPasswordDto } from './dto/forgot-password.dto';
23
25
  import { LoginDto } from './dto/login.dto';
@@ -25,6 +27,11 @@ import { RefreshTokenDto } from './dto/refresh-token.dto';
25
27
  import { RegisterDto } from './dto/register.dto';
26
28
  import { ResetPasswordDto } from './dto/reset-password.dto';
27
29
  import { VerifyEmailDto } from './dto/verify-email.dto';
30
+ import { GithubProfile } from './strategies/github.strategy';
31
+
32
+ interface RequestWithGithubUser extends Request {
33
+ user: GithubProfile;
34
+ }
28
35
 
29
36
  @ApiTags('Auth')
30
37
  @Controller('auth')
@@ -121,6 +128,45 @@ export class AuthController {
121
128
  return this.authService.getMe(user.userId);
122
129
  }
123
130
 
131
+ @Get('github')
132
+ @UseGuards(GithubAuthGuard)
133
+ @ApiOperation({ summary: 'Iniciar autenticación con GitHub' })
134
+ githubLogin(): void {}
135
+
136
+ @Get('github/callback')
137
+ @UseGuards(GithubAuthGuard)
138
+ @ApiOperation({ summary: 'Callback de GitHub OAuth' })
139
+ async githubCallback(
140
+ @Req() req: RequestWithGithubUser,
141
+ @Res() res: Response,
142
+ ): Promise<void> {
143
+ const frontendUrl = this.configService.getOrThrow<string>('FRONTEND_URL');
144
+ try {
145
+ const { code, alreadyExisted } = await this.authService.githubLogin(req.user);
146
+ const info = alreadyExisted ? '&info=already_exists' : '';
147
+ res.redirect(`${frontendUrl}/auth/github/callback?code=${code}${info}`);
148
+ } catch {
149
+ res.redirect(`${frontendUrl}/auth/github/callback?error=github_failed`);
150
+ }
151
+ }
152
+
153
+ @Post('github/exchange')
154
+ @HttpCode(HttpStatus.OK)
155
+ @Throttle({ default: { limit: 10, ttl: 60000 } })
156
+ @ApiOperation({ summary: 'Intercambiar código OAuth por sesión' })
157
+ async githubExchange(
158
+ @Body('code') code: string,
159
+ @Res({ passthrough: true }) res: Response,
160
+ ) {
161
+ if (!code) {
162
+ throw new UnauthorizedException('Código requerido.');
163
+ }
164
+ const { alreadyExisted, accessToken, refreshToken } =
165
+ await this.authService.exchangeGithubCode(code);
166
+ this.setTokenCookies(res, accessToken, refreshToken);
167
+ return { success: true, alreadyExisted };
168
+ }
169
+
124
170
  private setTokenCookies(
125
171
  res: Response,
126
172
  accessToken: string,
@@ -130,7 +176,8 @@ export class AuthController {
130
176
  const base = {
131
177
  httpOnly: true,
132
178
  secure: isProd,
133
- sameSite: 'strict' as const,
179
+ sameSite: (isProd ? 'none' : 'lax') as 'none' | 'lax',
180
+ ...(isProd && { partitioned: true }),
134
181
  };
135
182
 
136
183
  res.cookie('access_token', accessToken, {
@@ -150,7 +197,8 @@ export class AuthController {
150
197
  const base = {
151
198
  httpOnly: true,
152
199
  secure: isProd,
153
- sameSite: 'strict' as const,
200
+ sameSite: (isProd ? 'none' : 'lax') as 'none' | 'lax',
201
+ ...(isProd && { partitioned: true }),
154
202
  };
155
203
 
156
204
  res.clearCookie('access_token', base);
@@ -6,6 +6,7 @@ import { WorkspacesModule } from '../workspaces/workspaces.module';
6
6
  import { AuthController } from './auth.controller';
7
7
  import { AuthService } from './auth.service';
8
8
  import { JwtStrategy } from './strategies/jwt.strategy';
9
+ import { GithubStrategy } from './strategies/github.strategy';
9
10
 
10
11
  @Module({
11
12
  imports: [
@@ -15,6 +16,6 @@ import { JwtStrategy } from './strategies/jwt.strategy';
15
16
  WorkspacesModule,
16
17
  ],
17
18
  controllers: [AuthController],
18
- providers: [AuthService, JwtStrategy],
19
+ providers: [AuthService, JwtStrategy, GithubStrategy],
19
20
  })
20
21
  export class AuthModule {}