exp-node-server 1.1.7 → 2.0.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 (181) hide show
  1. package/README.md +82 -20
  2. package/dist/cli.js +525 -0
  3. package/package.json +29 -9
  4. package/templates/mongoose/.dockerignore +10 -0
  5. package/templates/mongoose/.env.example +56 -7
  6. package/templates/mongoose/.prettierignore +3 -0
  7. package/templates/mongoose/.prettierrc +7 -0
  8. package/templates/mongoose/Dockerfile +33 -0
  9. package/templates/mongoose/_gitignore +32 -0
  10. package/templates/mongoose/docker-compose.yml +21 -0
  11. package/templates/mongoose/eslint.config.js +35 -0
  12. package/templates/mongoose/package-lock.json +6377 -0
  13. package/templates/mongoose/package.json +49 -10
  14. package/templates/mongoose/readme.md +85 -0
  15. package/templates/mongoose/src/app/configs/index.ts +178 -26
  16. package/templates/mongoose/src/app/errors/zodError.ts +16 -16
  17. package/templates/mongoose/src/app/lib/db.ts +18 -0
  18. package/templates/mongoose/src/app/lib/logger.ts +12 -0
  19. package/templates/mongoose/src/app/middlewares/auth.ts +48 -44
  20. package/templates/mongoose/src/app/middlewares/global_error_handler.ts +59 -65
  21. package/templates/mongoose/src/app/middlewares/not_found_api.ts +7 -7
  22. package/templates/mongoose/src/app/middlewares/rate_limiter.ts +153 -0
  23. package/templates/mongoose/src/app/middlewares/request_validator.ts +11 -10
  24. package/templates/mongoose/src/app/middlewares/uploader.ts +17 -13
  25. package/templates/mongoose/src/app/modules/account/account.controller.ts +156 -0
  26. package/templates/mongoose/src/app/modules/account/account.interface.ts +65 -0
  27. package/templates/mongoose/src/app/modules/account/account.route.ts +101 -0
  28. package/templates/mongoose/src/app/modules/account/account.schema.ts +97 -0
  29. package/templates/mongoose/src/app/modules/account/account.service.ts +422 -0
  30. package/templates/mongoose/src/app/modules/account/account.swagger.ts +278 -0
  31. package/templates/mongoose/src/app/modules/account/account.validation.ts +63 -0
  32. package/templates/mongoose/src/app/modules/profile/profile.controller.ts +17 -0
  33. package/templates/mongoose/src/app/modules/profile/profile.interface.ts +7 -0
  34. package/templates/mongoose/src/app/modules/profile/profile.route.ts +28 -0
  35. package/templates/mongoose/src/app/modules/profile/profile.schema.ts +13 -0
  36. package/templates/mongoose/src/app/modules/profile/profile.service.ts +28 -0
  37. package/templates/mongoose/src/app/modules/profile/profile.swagger.ts +34 -0
  38. package/templates/mongoose/src/app/modules/profile/profile.validation.ts +8 -0
  39. package/templates/mongoose/src/app/passport-js/index.ts +87 -0
  40. package/templates/mongoose/src/app/passport-js/oauth-state.ts +150 -0
  41. package/templates/mongoose/src/app/passport-js/providers.ts +258 -0
  42. package/templates/mongoose/src/app/passport-js/social-account.ts +141 -0
  43. package/templates/mongoose/src/app/queues/agenda.ts +24 -0
  44. package/templates/mongoose/src/app/queues/email.queue.ts +53 -0
  45. package/templates/mongoose/src/app/queues/index.ts +13 -0
  46. package/templates/mongoose/src/app/templates/otpEmailTemplate.ts +185 -0
  47. package/templates/mongoose/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
  48. package/templates/mongoose/src/app/types/error.d.ts +7 -7
  49. package/templates/mongoose/src/app/types/express.d.ts +11 -0
  50. package/templates/mongoose/src/app/types/passport-oauth1.d.ts +51 -0
  51. package/templates/mongoose/src/app/utils/JWT.ts +38 -18
  52. package/templates/mongoose/src/app/utils/app_error.ts +10 -10
  53. package/templates/mongoose/src/app/utils/auth_cookie.ts +27 -0
  54. package/templates/mongoose/src/app/utils/catch_async.ts +8 -8
  55. package/templates/mongoose/src/app/utils/cloudinary.ts +26 -59
  56. package/templates/mongoose/src/app/utils/mail_sender.ts +26 -92
  57. package/templates/mongoose/src/app/utils/manage_response.ts +19 -19
  58. package/templates/mongoose/src/app/utils/otp.ts +20 -0
  59. package/templates/mongoose/src/app/utils/session.ts +55 -0
  60. package/templates/mongoose/src/app.ts +73 -42
  61. package/templates/mongoose/src/routes.ts +13 -13
  62. package/templates/mongoose/src/server.ts +38 -18
  63. package/templates/mongoose/src/swaggerOptions.ts +23 -23
  64. package/templates/mongoose/tests/app.test.ts +97 -0
  65. package/templates/mongoose/tests/global-setup.ts +15 -0
  66. package/templates/mongoose/tests/helpers/fixtures.ts +42 -0
  67. package/templates/mongoose/tests/routes/account.test.ts +614 -0
  68. package/templates/mongoose/tests/routes/profile.test.ts +95 -0
  69. package/templates/mongoose/tests/routes/social.test.ts +310 -0
  70. package/templates/mongoose/tests/setup.ts +25 -0
  71. package/templates/mongoose/tsconfig.json +16 -20
  72. package/templates/mongoose/vitest.config.ts +43 -0
  73. package/templates/prisma/.dockerignore +2 -0
  74. package/templates/prisma/.env.example +62 -6
  75. package/templates/prisma/.prettierignore +3 -0
  76. package/templates/prisma/.prettierrc +7 -0
  77. package/templates/prisma/.projectrc.json +1 -1
  78. package/templates/prisma/Dockerfile +21 -7
  79. package/templates/prisma/_gitignore +35 -0
  80. package/templates/prisma/docker-compose.yml +8 -7
  81. package/templates/prisma/eslint.config.js +35 -0
  82. package/templates/prisma/package-lock.json +7327 -0
  83. package/templates/prisma/package.json +47 -9
  84. package/templates/prisma/prisma/schema/account.schema.prisma +92 -10
  85. package/templates/prisma/prisma/schema/profile.schema.prisma +8 -3
  86. package/templates/prisma/prisma/schema/schema.prisma +1 -2
  87. package/templates/prisma/prisma.config.ts +5 -5
  88. package/templates/prisma/readme.md +81 -0
  89. package/templates/prisma/src/app/configs/index.ts +175 -26
  90. package/templates/prisma/src/app/errors/zodError.ts +16 -16
  91. package/templates/prisma/src/app/lib/logger.ts +12 -0
  92. package/templates/prisma/src/app/lib/prisma.ts +4 -6
  93. package/templates/prisma/src/app/middlewares/auth.ts +37 -16
  94. package/templates/prisma/src/app/middlewares/global_error_handler.ts +42 -35
  95. package/templates/prisma/src/app/middlewares/not_found_api.ts +7 -7
  96. package/templates/prisma/src/app/middlewares/rate_limiter.ts +124 -0
  97. package/templates/prisma/src/app/middlewares/request_validator.ts +4 -3
  98. package/templates/prisma/src/app/middlewares/uploader.ts +17 -13
  99. package/templates/prisma/src/app/modules/account/account.controller.ts +74 -34
  100. package/templates/prisma/src/app/modules/account/account.route.ts +68 -22
  101. package/templates/prisma/src/app/modules/account/account.service.ts +329 -309
  102. package/templates/prisma/src/app/modules/account/account.swagger.ts +167 -76
  103. package/templates/prisma/src/app/modules/account/account.validation.ts +41 -19
  104. package/templates/prisma/src/app/modules/profile/profile.controller.ts +4 -4
  105. package/templates/prisma/src/app/modules/profile/profile.route.ts +18 -12
  106. package/templates/prisma/src/app/modules/profile/profile.service.ts +11 -18
  107. package/templates/prisma/src/app/modules/profile/profile.swagger.ts +11 -11
  108. package/templates/prisma/src/app/modules/profile/profile.validation.ts +2 -2
  109. package/templates/prisma/src/app/passport-js/index.ts +87 -0
  110. package/templates/prisma/src/app/passport-js/oauth-state.ts +150 -0
  111. package/templates/prisma/src/app/passport-js/providers.ts +263 -0
  112. package/templates/prisma/src/app/passport-js/social-account.ts +147 -0
  113. package/templates/prisma/src/app/queues/boss.ts +8 -0
  114. package/templates/prisma/src/app/queues/email.queue.ts +56 -0
  115. package/templates/prisma/src/app/queues/index.ts +14 -0
  116. package/templates/prisma/src/app/queues/maintenance.queue.ts +30 -0
  117. package/templates/prisma/src/app/templates/otpEmailTemplate.ts +185 -0
  118. package/templates/prisma/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
  119. package/templates/prisma/src/app/types/error.d.ts +7 -7
  120. package/templates/prisma/src/app/types/express.d.ts +11 -0
  121. package/templates/prisma/src/app/types/passport-oauth1.d.ts +51 -0
  122. package/templates/prisma/src/app/utils/JWT.ts +36 -17
  123. package/templates/prisma/src/app/utils/app_error.ts +10 -10
  124. package/templates/prisma/src/app/utils/auth_cookie.ts +27 -0
  125. package/templates/prisma/src/app/utils/catch_async.ts +8 -8
  126. package/templates/prisma/src/app/utils/cloudinary.ts +26 -59
  127. package/templates/prisma/src/app/utils/mail_sender.ts +26 -92
  128. package/templates/prisma/src/app/utils/manage_response.ts +19 -19
  129. package/templates/prisma/src/app/utils/otp.ts +20 -0
  130. package/templates/prisma/src/app/utils/session.ts +57 -0
  131. package/templates/prisma/src/app.ts +73 -42
  132. package/templates/prisma/src/routes.ts +12 -10
  133. package/templates/prisma/src/server.ts +37 -19
  134. package/templates/prisma/src/swaggerOptions.ts +22 -26
  135. package/templates/prisma/tests/app.test.ts +86 -0
  136. package/templates/prisma/tests/helpers/fixtures.ts +35 -0
  137. package/templates/prisma/tests/helpers/prisma.ts +32 -0
  138. package/templates/prisma/tests/routes/account.test.ts +627 -0
  139. package/templates/prisma/tests/routes/profile.test.ts +91 -0
  140. package/templates/prisma/tests/routes/social.test.ts +317 -0
  141. package/templates/prisma/tests/setup.ts +10 -0
  142. package/templates/prisma/tsconfig.json +16 -23
  143. package/templates/prisma/vitest.config.ts +38 -0
  144. package/bin/cli.js +0 -41
  145. package/src/createProject.js +0 -61
  146. package/src/generateModule.js +0 -93
  147. package/src/utils/getProjectType.js +0 -13
  148. package/src/utils/mongoose/controllerGenerator.js +0 -70
  149. package/src/utils/mongoose/routeGenerator.js +0 -27
  150. package/src/utils/mongoose/schemaGenerator.js +0 -11
  151. package/src/utils/mongoose/serviceGenerator.js +0 -49
  152. package/src/utils/mongoose/swaggerGenerator.js +0 -112
  153. package/src/utils/mongoose/validationGenerator.js +0 -13
  154. package/src/utils/prisma/prismaSchemaGenerator.js +0 -10
  155. package/src/utils/prisma/prismaServiceGenerator.js +0 -47
  156. package/src/utils/routeInjector.js +0 -40
  157. package/src/utils/swaggerInjector.js +0 -52
  158. package/src/utils/validateModuleName.js +0 -17
  159. package/src/utils/validateProjectName.js +0 -15
  160. package/src/utils.js +0 -26
  161. package/templates/mongoose/src/app/errors/castError.ts +0 -23
  162. package/templates/mongoose/src/app/errors/duplicateError.ts +0 -26
  163. package/templates/mongoose/src/app/errors/validationError.ts +0 -25
  164. package/templates/mongoose/src/app/modules/auth/auth.controller.ts +0 -127
  165. package/templates/mongoose/src/app/modules/auth/auth.interface.ts +0 -24
  166. package/templates/mongoose/src/app/modules/auth/auth.route.ts +0 -46
  167. package/templates/mongoose/src/app/modules/auth/auth.schema.ts +0 -19
  168. package/templates/mongoose/src/app/modules/auth/auth.service.ts +0 -324
  169. package/templates/mongoose/src/app/modules/auth/auth.swagger.ts +0 -214
  170. package/templates/mongoose/src/app/modules/auth/auth.validation.ts +0 -37
  171. package/templates/mongoose/src/app/modules/user/user.controller.ts +0 -17
  172. package/templates/mongoose/src/app/modules/user/user.interface.ts +0 -15
  173. package/templates/mongoose/src/app/modules/user/user.route.ts +0 -20
  174. package/templates/mongoose/src/app/modules/user/user.schema.ts +0 -22
  175. package/templates/mongoose/src/app/modules/user/user.service.ts +0 -22
  176. package/templates/mongoose/src/app/modules/user/user.swagger.ts +0 -75
  177. package/templates/mongoose/src/app/modules/user/user.validation.ts +0 -18
  178. package/templates/mongoose/src/app/types/index.d.ts +0 -12
  179. package/templates/mongoose/src/app/utils/isAccountExist.ts +0 -31
  180. package/templates/prisma/src/app/types/index.d.ts +0 -12
  181. package/templates/prisma/src/app/utils/otpGenerator.ts +0 -29
@@ -4,40 +4,79 @@
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
8
- "dev": "ts-node-dev --respawn --transpile-only src/server.ts",
9
- "build": "tsc"
7
+ "test": "vitest run",
8
+ "test:watch": "vitest",
9
+ "dev": "tsx watch src/server.ts",
10
+ "build": "tsc",
11
+ "lint": "eslint .",
12
+ "lint:fix": "eslint . --fix",
13
+ "format": "prettier --write .",
14
+ "check": "tsc --noEmit"
10
15
  },
11
16
  "keywords": [],
12
17
  "author": "",
13
18
  "license": "ISC",
14
- "type": "commonjs",
19
+ "type": "module",
15
20
  "dependencies": {
21
+ "@agendajs/mongo-backend": "^4.0.3",
22
+ "agenda": "^6.2.6",
16
23
  "bcrypt": "^6.0.0",
17
24
  "cloudinary": "^2.7.0",
18
25
  "cookie-parser": "^1.4.7",
19
26
  "cors": "^2.8.5",
20
- "dotenv": "^17.2.3",
27
+ "dotenv": "^17.4.2",
21
28
  "express": "^5.1.0",
29
+ "express-rate-limit": "^8.7.1",
30
+ "helmet": "^8.3.0",
22
31
  "jsonwebtoken": "^9.0.2",
32
+ "mongodb": "^7.7.0",
33
+ "mongoose": "^9.11.2",
23
34
  "multer": "^2.0.2",
24
- "nodemailer": "^7.0.9",
35
+ "nodemailer": "^8.0.5",
36
+ "passport": "^0.7.0",
37
+ "passport-facebook": "^3.0.0",
38
+ "passport-github": "^1.1.0",
39
+ "passport-google-oauth20": "^2.0.0",
40
+ "passport-oauth1": "^1.3.0",
41
+ "passport-oauth2": "^1.8.0",
42
+ "pino": "^10.3.1",
25
43
  "swagger-jsdoc": "^6.2.8",
26
44
  "swagger-ui-express": "^5.0.1",
27
- "ts-node-dev": "^2.0.0",
28
45
  "zod": "^4.1.12"
29
46
  },
30
47
  "devDependencies": {
48
+ "@eslint/js": "^10.0.1",
31
49
  "@types/bcrypt": "^6.0.0",
32
50
  "@types/cookie-parser": "^1.4.9",
33
51
  "@types/cors": "^2.8.19",
34
52
  "@types/express": "^5.0.3",
35
53
  "@types/jsonwebtoken": "^9.0.10",
36
- "@types/mongoose": "^5.11.96",
37
54
  "@types/multer": "^2.0.0",
38
- "@types/node": "^24.10.9",
55
+ "@types/node": "^24.12.2",
39
56
  "@types/nodemailer": "^7.0.2",
57
+ "@types/passport": "^1.0.17",
58
+ "@types/passport-facebook": "^3.0.4",
59
+ "@types/passport-github": "^1.1.13",
60
+ "@types/passport-google-oauth20": "^2.0.17",
61
+ "@types/passport-oauth2": "^1.8.0",
62
+ "@types/supertest": "^7.2.1",
40
63
  "@types/swagger-jsdoc": "^6.0.4",
41
- "@types/swagger-ui-express": "^4.1.8"
64
+ "@types/swagger-ui-express": "^4.1.8",
65
+ "@typescript-eslint/eslint-plugin": "^8.59.0",
66
+ "@typescript-eslint/parser": "^8.59.0",
67
+ "eslint": "^10.2.1",
68
+ "eslint-config-prettier": "^10.1.8",
69
+ "eslint-plugin-prettier": "^5.5.5",
70
+ "mongodb-memory-server": "^11.3.0",
71
+ "pino-pretty": "^13.1.3",
72
+ "prettier": "^3.8.3",
73
+ "supertest": "^7.3.1",
74
+ "tsx": "^4.21.0",
75
+ "typescript": "^5.9.3",
76
+ "typescript-eslint": "^8.59.0",
77
+ "vitest": "^5.0.4"
78
+ },
79
+ "engines": {
80
+ "node": ">=22.12.0"
42
81
  }
43
82
  }
@@ -93,9 +93,94 @@ Swagger docs available at:
93
93
  http://localhost:5000/docs
94
94
  ```
95
95
 
96
+ ## 🔐 Environment
97
+
98
+ Copy `.env.example` to `.env` and fill it in. The app validates its environment
99
+ on startup and exits with a list of anything missing or invalid.
100
+
101
+ `ACCESS_TOKEN`, `VERIFIED_TOKEN` and `RESET_SECRET` must be **different** random
102
+ values of at least 32 characters:
103
+
104
+ ```bash
105
+ openssl rand -hex 32
106
+ ```
107
+
108
+ ## 🗄️ Database
109
+
110
+ `docker compose up -d` starts MongoDB on `localhost:27017` (matching `DATABASE_URL`
111
+ in `.env.example`). Indexes (unique email, TTLs) are created automatically on start.
112
+
113
+ ## 📬 Background Jobs
114
+
115
+ Emails (account verification and password reset) are sent through a single
116
+ `email` job powered by [Agenda](https://github.com/agenda/agenda). Jobs are stored
117
+ in the same MongoDB (`agendaJobs` collection), so no Redis is needed. Failed sends
118
+ are retried with exponential backoff, and finished jobs are deleted because they
119
+ carry OTPs and reset links.
120
+
121
+ To add a new email, add a variant to `EmailJob` in `src/app/queues/email.queue.ts`
122
+ and handle it in `render()`.
123
+
124
+ ## 🔐 Sessions & Refresh Tokens
125
+
126
+ Signing in (password or social) returns a short-lived **access token** (`ACCESS_EXPIRES`,
127
+ 15 minutes by default) and a **refresh token** (`REFRESH_EXPIRES_DAYS`, 30 by default).
128
+ Both are also set as httpOnly cookies; the refresh cookie is only sent to `/api/auth`.
129
+
130
+ - `POST /api/auth/refresh-token`: when the access token expires (401), get new tokens.
131
+ Browsers send the cookie automatically; other clients send `{ "refreshToken": "..." }`.
132
+ - `POST /api/auth/sign-out`: ends this device's session.
133
+ - `POST /api/auth/sign-out-all`: ends every session (also happens on password change/reset).
134
+
135
+ Refresh tokens are random strings stored only as hashes, and are **rotated** on every
136
+ refresh. If an already-used refresh token shows up again, it was most likely stolen,
137
+ so the whole session is revoked. (A reuse within 30 seconds is treated as two tabs
138
+ refreshing at once and only that request fails.)
139
+
140
+ ## 🔑 Social Login
141
+
142
+ Supported: **Google, Facebook, GitHub, X (Twitter), LinkedIn and Discord**. Set
143
+ `IS_<PROVIDER>_LOGIN=true` and fill in the credentials in `.env`.
144
+
145
+ - `GET /api/auth/social/providers`: enabled providers, for rendering buttons
146
+ - `GET /api/auth/social/<provider>?returnTo=/dashboard`: start the login (a link, not `fetch`)
147
+ - `GET /api/auth/social/<provider>/callback`: OAuth callback (register this URL at the provider)
148
+ - `POST /api/auth/social/exchange` `{ "code": "..." }`: trade the code for an access token
149
+
150
+ After the provider, the browser lands on `SOCIAL_REDIRECT_URL` (default
151
+ `FRONT_END_URL/auth/social/callback`) with either `?code=...&returnTo=...` or
152
+ `?error=...`. The front-end posts the code (single-use, valid for 60 seconds) to
153
+ `/exchange`, which returns the access token and sets the cookie, like `/sign-in`.
154
+
155
+ Every login is protected by an OAuth `state` (and PKCE for Google), stored in a
156
+ short-lived encrypted cookie, so no session store is needed. Identities are linked
157
+ by the provider's user id, and only verified provider emails are used to create or
158
+ link accounts. Providers are defined in `src/app/passport-js/providers.ts`.
159
+
160
+ ## 🛡️ Rate Limiting
161
+
162
+ The auth endpoints have their own limits (sign-in per IP + email, emails per
163
+ recipient, OTP / reset guesses per IP), stored in the `rate_limits` collection so they are
164
+ shared by every instance — no Redis needed. A TTL index removes expired counters.
165
+ The whole API also has a lenient per-instance in-memory limit. Tune them in
166
+ `src/app/middlewares/rate_limiter.ts`, and set `TRUST_PROXY` when running behind a
167
+ proxy so the real client IP is used.
168
+
169
+ ## 🧪 Tests
170
+
171
+ ```bash
172
+ npm test
173
+ ```
174
+
175
+ Route tests with [supertest](https://github.com/ladjs/supertest) run each request
176
+ through the route, controller and service against a real in-memory MongoDB
177
+ ([mongodb-memory-server](https://github.com/typegoose/mongodb-memory-server), downloaded
178
+ on the first run). Only the email queue and Cloudinary are mocked.
179
+
96
180
  ## 🧠 Tech Stack
97
181
 
98
182
  - Express.js — Backend framework
183
+ - Agenda — MongoDB job queue
99
184
  - TypeScript — Strongly typed JavaScript
100
185
  - Mongoose — MongoDB ODM
101
186
  - Prisma — PostgreSQL ORM
@@ -1,26 +1,178 @@
1
- import "dotenv/config";
2
-
3
- export const configs = {
4
- port: process.env.PORT,
5
- env: process.env.NODE_ENV,
6
- db_url: process.env.DATABASE_URL,
7
- jwt: {
8
- access_token: process.env.ACCESS_TOKEN,
9
- refresh_token: process.env.REFRESH_TOKEN,
10
- access_expires: process.env.ACCESS_EXPIRES,
11
- refresh_expires: process.env.REFRESH_EXPIRES,
12
- reset_secret: process.env.RESET_SECRET,
13
- reset_expires: process.env.RESET_EXPIRES,
14
- front_end_url: process.env.FRONT_END_URL,
15
- verified_token: process.env.VERIFIED_TOKEN,
16
- },
17
- email: {
18
- app_email: process.env.APP_USER_EMAIL,
19
- app_password: process.env.APP_PASSWORD,
20
- },
21
- cloudinary: {
22
- cloud_name: process.env.CLOUD_NAME,
23
- cloud_api_key: process.env.CLOUD_API_KEY,
24
- cloud_api_secret: process.env.CLOUD_API_SECRET,
25
- },
26
- };
1
+ import 'dotenv/config';
2
+ import { z } from 'zod';
3
+
4
+ const bool = z
5
+ .enum(['true', 'false'])
6
+ .default('false')
7
+ .transform((v) => v === 'true');
8
+
9
+ // JWT/session secrets must be long and random, e.g. `openssl rand -hex 32`
10
+ const secret = z.string().min(32, 'must be at least 32 characters');
11
+
12
+ const envSchema = z.object({
13
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
14
+ PORT: z.coerce.number().int().positive().default(5000),
15
+ // mongodb:// or mongodb+srv:// (replica set host lists aren't valid URLs, so no z.url())
16
+ DATABASE_URL: z
17
+ .string()
18
+ .regex(/^mongodb(\+srv)?:\/\/\S+$/, 'must be a MongoDB connection string'),
19
+
20
+ // auth
21
+ ACCESS_TOKEN: secret,
22
+ // short-lived; clients get a new one from POST /api/auth/refresh-token
23
+ ACCESS_EXPIRES: z.string().default('15m'),
24
+ REFRESH_EXPIRES_DAYS: z.coerce.number().int().positive().default(30),
25
+ VERIFIED_TOKEN: secret,
26
+ RESET_SECRET: secret,
27
+ RESET_EXPIRES: z.string().default('15m'),
28
+ FRONT_END_URL: z.url(),
29
+ CORS_ORIGINS: z.string().optional(),
30
+ TRUST_PROXY: z.coerce.number().int().min(0).default(0),
31
+
32
+ // email
33
+ APP_USER_EMAIL: z.string().optional(),
34
+ APP_PASSWORD: z.string().optional(),
35
+ MAIL_FROM: z.string().optional(),
36
+
37
+ // cloudinary
38
+ CLOUD_NAME: z.string().optional(),
39
+ CLOUD_API_KEY: z.string().optional(),
40
+ CLOUD_API_SECRET: z.string().optional(),
41
+
42
+ // swagger
43
+ ENABLE_SWAGGER: z.enum(['true', 'false']).optional(),
44
+ DEVELOPMENT_BASE_URL: z.string().default('http://localhost:5000'),
45
+ PRODUCTION_BASE_URL: z.string().default(''),
46
+
47
+ // social login
48
+ // where the browser is sent after a social login, with `?code=` or `?error=`
49
+ SOCIAL_REDIRECT_URL: z.url().optional(),
50
+
51
+ IS_GOOGLE_LOGIN: bool,
52
+ GOOGLE_CLIENT_ID: z.string().optional(),
53
+ GOOGLE_CLIENT_SECRET: z.string().optional(),
54
+ GOOGLE_CALLBACK_URL: z.string().optional(),
55
+
56
+ IS_FACEBOOK_LOGIN: bool,
57
+ FACEBOOK_APP_ID: z.string().optional(),
58
+ FACEBOOK_APP_SECRET: z.string().optional(),
59
+ FACEBOOK_CALLBACK_URL: z.string().optional(),
60
+
61
+ IS_GITHUB_LOGIN: bool,
62
+ GITHUB_CLIENT_ID: z.string().optional(),
63
+ GITHUB_CLIENT_SECRET: z.string().optional(),
64
+ GITHUB_CALLBACK_URL: z.string().optional(),
65
+
66
+ IS_TWITTER_LOGIN: bool,
67
+ TWITTER_CONSUMER_KEY: z.string().optional(),
68
+ TWITTER_CONSUMER_SECRET: z.string().optional(),
69
+ TWITTER_CALLBACK_URL: z.string().optional(),
70
+
71
+ IS_LINKEDIN_LOGIN: bool,
72
+ LINKEDIN_CLIENT_ID: z.string().optional(),
73
+ LINKEDIN_CLIENT_SECRET: z.string().optional(),
74
+ LINKEDIN_CALLBACK_URL: z.string().optional(),
75
+
76
+ IS_DISCORD_LOGIN: bool,
77
+ DISCORD_CLIENT_ID: z.string().optional(),
78
+ DISCORD_CLIENT_SECRET: z.string().optional(),
79
+ DISCORD_CALLBACK_URL: z.string().optional(),
80
+ });
81
+
82
+ type Env = z.infer<typeof envSchema>;
83
+
84
+ // logger depends on configs, so failures here are reported with console
85
+ const fail = (message: string): never => {
86
+ // eslint-disable-next-line no-console
87
+ console.error(message);
88
+ process.exit(1);
89
+ };
90
+
91
+ const parsed = envSchema.safeParse(process.env);
92
+ if (!parsed.success) {
93
+ const issues = parsed.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`);
94
+ fail(`Invalid environment variables:\n${issues.join('\n')}`);
95
+ }
96
+ const env = parsed.data as Env;
97
+
98
+ /** Credentials for a social provider; all are required once the provider is enabled. */
99
+ const provider = (enabled: boolean, keys: [keyof Env, keyof Env, keyof Env]) => {
100
+ const [clientId, clientSecret, callbackUrl] = keys.map((key) => {
101
+ const value = env[key];
102
+ if (enabled && (typeof value !== 'string' || !value)) {
103
+ fail(`${key} is required when its login provider is enabled`);
104
+ }
105
+ return typeof value === 'string' ? value : '';
106
+ }) as [string, string, string];
107
+
108
+ return { enabled, client_id: clientId, client_secret: clientSecret, callback_url: callbackUrl };
109
+ };
110
+
111
+ export const configs = {
112
+ port: env.PORT,
113
+ env: env.NODE_ENV,
114
+ is_production: env.NODE_ENV === 'production',
115
+ is_test: env.NODE_ENV === 'test',
116
+ db_url: env.DATABASE_URL,
117
+ cors_origins: (env.CORS_ORIGINS ?? env.FRONT_END_URL).split(',').map((o) => o.trim()),
118
+ trust_proxy: env.TRUST_PROXY,
119
+ enable_swagger: env.ENABLE_SWAGGER
120
+ ? env.ENABLE_SWAGGER === 'true'
121
+ : env.NODE_ENV !== 'production',
122
+ jwt: {
123
+ access_token: env.ACCESS_TOKEN,
124
+ access_expires: env.ACCESS_EXPIRES,
125
+ refresh_expires_days: env.REFRESH_EXPIRES_DAYS,
126
+ reset_secret: env.RESET_SECRET,
127
+ reset_expires: env.RESET_EXPIRES,
128
+ verified_token: env.VERIFIED_TOKEN,
129
+ front_end_url: env.FRONT_END_URL,
130
+ },
131
+ email: {
132
+ app_email: env.APP_USER_EMAIL,
133
+ app_password: env.APP_PASSWORD,
134
+ from: env.MAIL_FROM ?? env.APP_USER_EMAIL,
135
+ },
136
+ cloudinary: {
137
+ cloud_name: env.CLOUD_NAME,
138
+ cloud_api_key: env.CLOUD_API_KEY,
139
+ cloud_api_secret: env.CLOUD_API_SECRET,
140
+ },
141
+ swagger_config: {
142
+ development_base_url: env.DEVELOPMENT_BASE_URL,
143
+ production_base_url: env.PRODUCTION_BASE_URL,
144
+ },
145
+ social: {
146
+ redirect_url: env.SOCIAL_REDIRECT_URL ?? `${env.FRONT_END_URL}/auth/social/callback`,
147
+ google: provider(env.IS_GOOGLE_LOGIN, [
148
+ 'GOOGLE_CLIENT_ID',
149
+ 'GOOGLE_CLIENT_SECRET',
150
+ 'GOOGLE_CALLBACK_URL',
151
+ ]),
152
+ facebook: provider(env.IS_FACEBOOK_LOGIN, [
153
+ 'FACEBOOK_APP_ID',
154
+ 'FACEBOOK_APP_SECRET',
155
+ 'FACEBOOK_CALLBACK_URL',
156
+ ]),
157
+ github: provider(env.IS_GITHUB_LOGIN, [
158
+ 'GITHUB_CLIENT_ID',
159
+ 'GITHUB_CLIENT_SECRET',
160
+ 'GITHUB_CALLBACK_URL',
161
+ ]),
162
+ twitter: provider(env.IS_TWITTER_LOGIN, [
163
+ 'TWITTER_CONSUMER_KEY',
164
+ 'TWITTER_CONSUMER_SECRET',
165
+ 'TWITTER_CALLBACK_URL',
166
+ ]),
167
+ linkedin: provider(env.IS_LINKEDIN_LOGIN, [
168
+ 'LINKEDIN_CLIENT_ID',
169
+ 'LINKEDIN_CLIENT_SECRET',
170
+ 'LINKEDIN_CALLBACK_URL',
171
+ ]),
172
+ discord: provider(env.IS_DISCORD_LOGIN, [
173
+ 'DISCORD_CLIENT_ID',
174
+ 'DISCORD_CLIENT_SECRET',
175
+ 'DISCORD_CALLBACK_URL',
176
+ ]),
177
+ },
178
+ };
@@ -1,20 +1,20 @@
1
- import { ZodError, ZodIssue } from 'zod'
2
- import { TErrorSources, TGenericErrorResponse } from '../types/error'
1
+ import { ZodError, ZodIssue } from 'zod';
2
+ import { TErrorSources, TGenericErrorResponse } from '../types/error.js';
3
3
 
4
4
  const handleZodError = (err: ZodError): TGenericErrorResponse => {
5
- const errorSources: TErrorSources = err.issues.map((issue: ZodIssue) => {
6
- return {
7
- path: issue?.path[issue.path.length - 1] as string,
8
- message: issue.message
9
- }
10
- })
11
-
12
- const statusCode = 400
5
+ const errorSources: TErrorSources = err.issues.map((issue: ZodIssue) => {
13
6
  return {
14
- statusCode,
15
- message: 'Validation Error',
16
- errorSources
17
- }
18
- }
7
+ path: issue?.path[issue.path.length - 1] as string,
8
+ message: issue.message,
9
+ };
10
+ });
11
+
12
+ const statusCode = 400;
13
+ return {
14
+ statusCode,
15
+ message: 'Validation Error',
16
+ errorSources,
17
+ };
18
+ };
19
19
 
20
- export default handleZodError
20
+ export default handleZodError;
@@ -0,0 +1,18 @@
1
+ import mongoose from 'mongoose';
2
+ import { configs } from '../configs/index.js';
3
+ import { logger } from './logger.js';
4
+
5
+ // reject unknown fields in queries instead of silently ignoring them
6
+ mongoose.set('strictQuery', true);
7
+
8
+ export const connectDatabase = async () => {
9
+ await mongoose.connect(configs.db_url, {
10
+ // fail fast on startup instead of buffering queries forever
11
+ serverSelectionTimeoutMS: 10_000,
12
+ // autoIndex stays on: the unique email and TTL indexes are needed for correctness
13
+ });
14
+ mongoose.connection.on('error', (err) => logger.error(err, 'MongoDB error'));
15
+ logger.info('Connected to MongoDB');
16
+ };
17
+
18
+ export const disconnectDatabase = () => mongoose.disconnect();
@@ -0,0 +1,12 @@
1
+ import pino from 'pino';
2
+ import { configs } from '../configs/index.js';
3
+
4
+ export const logger = pino({
5
+ level: configs.is_test ? 'silent' : configs.env === 'development' ? 'debug' : 'info',
6
+ transport:
7
+ configs.env === 'development'
8
+ ? {
9
+ target: 'pino-pretty',
10
+ }
11
+ : undefined,
12
+ });
@@ -1,50 +1,54 @@
1
- import { NextFunction, Request, Response } from 'express';
2
- import { AppError } from '../utils/app_error';
3
- import { configs } from '../configs';
4
- import { jwtHelpers, JwtPayloadType } from '../utils/JWT';
5
- import { Account_Model } from '../modules/auth/auth.schema';
1
+ import { isValidObjectId } from 'mongoose';
2
+ import type { NextFunction, Request, Response } from 'express';
3
+ import { configs } from '../configs/index.js';
4
+ import type { ROLE } from '../modules/account/account.interface.js';
5
+ import { account_model } from '../modules/account/account.schema.js';
6
+ import { AppError } from '../utils/app_error.js';
7
+ import { ACCESS_TOKEN_COOKIE } from '../utils/auth_cookie.js';
8
+ import { type JwtPayloadType, jwtHelpers } from '../utils/JWT.js';
6
9
 
10
+ const getToken = (req: Request): string | undefined => {
11
+ const header = req.headers.authorization;
12
+ if (header) return header.startsWith('Bearer ') ? header.slice(7) : header;
13
+ return req.cookies?.[ACCESS_TOKEN_COOKIE];
14
+ };
15
+
16
+ /**
17
+ * Requires a valid access token. With roles, also requires one of them:
18
+ * `auth()` = any signed-in user, `auth('ADMIN')` = admins only.
19
+ */
20
+ const auth = (...roles: ROLE[]) => {
21
+ return async (req: Request, _res: Response, next: NextFunction) => {
22
+ try {
23
+ const token = getToken(req);
24
+ if (!token) {
25
+ throw new AppError('You are not authorized', 401);
26
+ }
27
+
28
+ const payload = jwtHelpers.verifyToken<JwtPayloadType>(token, configs.jwt.access_token);
7
29
 
8
- type Role = "ADMIN" | "USER"
30
+ // tokens outlive account changes, so check the account is still active and
31
+ // the token wasn't revoked (password change/reset, sign out everywhere)
32
+ const account = isValidObjectId(payload.accountId)
33
+ ? await account_model
34
+ .findById(payload.accountId)
35
+ .select('email role isDeleted tokenVersion')
36
+ .lean()
37
+ : null;
38
+ if (!account || account.isDeleted || account.tokenVersion !== payload.tv) {
39
+ throw new AppError('You are not authorized', 401);
40
+ }
9
41
 
42
+ if (roles.length && !roles.includes(account.role)) {
43
+ throw new AppError('You do not have permission to perform this action', 403);
44
+ }
10
45
 
11
- const auth = (...roles: Role[]) => {
12
- return async (req: Request, res: Response, next: NextFunction) => {
13
- try {
14
- const token = req.headers.authorization;
15
- if (!token) {
16
- throw new AppError('You are not authorize!!', 401);
17
- }
18
- const verifiedUser = jwtHelpers.verifyToken(
19
- token,
20
- configs.jwt.access_token as string,
21
- );
22
- if (!roles.length || !roles.includes(verifiedUser.role)) {
23
- throw new AppError('You are not authorize!!', 401);
24
- }
25
- // check user
26
- const isUserExist = await Account_Model.findOne({ email: verifiedUser?.email }).lean()
27
- if (!isUserExist) {
28
- throw new AppError("Account not found !", 404)
29
- }
30
- if (isUserExist?.accountStatus == "SUSPENDED") {
31
- throw new AppError("This Account is suspended !", 401)
32
- }
33
- if (isUserExist?.accountStatus == "INACTIVE") {
34
- throw new AppError("This Account is inactive !", 401)
35
- }
36
- if (isUserExist?.isDeleted) {
37
- throw new AppError("This account is deleted", 401)
38
- }
39
- if (!isUserExist?.isVerified) {
40
- throw new AppError("This account is not verified ", 401)
41
- }
42
- req.user = verifiedUser as JwtPayloadType;
43
- next();
44
- } catch (err) {
45
- next(err);
46
- }
47
- };
46
+ req.user = { accountId: String(account._id), email: account.email, role: account.role };
47
+ next();
48
+ } catch (err) {
49
+ next(err);
50
+ }
51
+ };
48
52
  };
49
53
 
50
- export default auth;
54
+ export default auth;