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.
- package/README.md +82 -20
- package/dist/cli.js +525 -0
- package/package.json +29 -9
- package/templates/mongoose/.dockerignore +10 -0
- package/templates/mongoose/.env.example +56 -7
- package/templates/mongoose/.prettierignore +3 -0
- package/templates/mongoose/.prettierrc +7 -0
- package/templates/mongoose/Dockerfile +33 -0
- package/templates/mongoose/_gitignore +32 -0
- package/templates/mongoose/docker-compose.yml +21 -0
- package/templates/mongoose/eslint.config.js +35 -0
- package/templates/mongoose/package-lock.json +6377 -0
- package/templates/mongoose/package.json +49 -10
- package/templates/mongoose/readme.md +85 -0
- package/templates/mongoose/src/app/configs/index.ts +178 -26
- package/templates/mongoose/src/app/errors/zodError.ts +16 -16
- package/templates/mongoose/src/app/lib/db.ts +18 -0
- package/templates/mongoose/src/app/lib/logger.ts +12 -0
- package/templates/mongoose/src/app/middlewares/auth.ts +48 -44
- package/templates/mongoose/src/app/middlewares/global_error_handler.ts +59 -65
- package/templates/mongoose/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/mongoose/src/app/middlewares/rate_limiter.ts +153 -0
- package/templates/mongoose/src/app/middlewares/request_validator.ts +11 -10
- package/templates/mongoose/src/app/middlewares/uploader.ts +17 -13
- package/templates/mongoose/src/app/modules/account/account.controller.ts +156 -0
- package/templates/mongoose/src/app/modules/account/account.interface.ts +65 -0
- package/templates/mongoose/src/app/modules/account/account.route.ts +101 -0
- package/templates/mongoose/src/app/modules/account/account.schema.ts +97 -0
- package/templates/mongoose/src/app/modules/account/account.service.ts +422 -0
- package/templates/mongoose/src/app/modules/account/account.swagger.ts +278 -0
- package/templates/mongoose/src/app/modules/account/account.validation.ts +63 -0
- package/templates/mongoose/src/app/modules/profile/profile.controller.ts +17 -0
- package/templates/mongoose/src/app/modules/profile/profile.interface.ts +7 -0
- package/templates/mongoose/src/app/modules/profile/profile.route.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.schema.ts +13 -0
- package/templates/mongoose/src/app/modules/profile/profile.service.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.swagger.ts +34 -0
- package/templates/mongoose/src/app/modules/profile/profile.validation.ts +8 -0
- package/templates/mongoose/src/app/passport-js/index.ts +87 -0
- package/templates/mongoose/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/mongoose/src/app/passport-js/providers.ts +258 -0
- package/templates/mongoose/src/app/passport-js/social-account.ts +141 -0
- package/templates/mongoose/src/app/queues/agenda.ts +24 -0
- package/templates/mongoose/src/app/queues/email.queue.ts +53 -0
- package/templates/mongoose/src/app/queues/index.ts +13 -0
- package/templates/mongoose/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/mongoose/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/mongoose/src/app/types/error.d.ts +7 -7
- package/templates/mongoose/src/app/types/express.d.ts +11 -0
- package/templates/mongoose/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/mongoose/src/app/utils/JWT.ts +38 -18
- package/templates/mongoose/src/app/utils/app_error.ts +10 -10
- package/templates/mongoose/src/app/utils/auth_cookie.ts +27 -0
- package/templates/mongoose/src/app/utils/catch_async.ts +8 -8
- package/templates/mongoose/src/app/utils/cloudinary.ts +26 -59
- package/templates/mongoose/src/app/utils/mail_sender.ts +26 -92
- package/templates/mongoose/src/app/utils/manage_response.ts +19 -19
- package/templates/mongoose/src/app/utils/otp.ts +20 -0
- package/templates/mongoose/src/app/utils/session.ts +55 -0
- package/templates/mongoose/src/app.ts +73 -42
- package/templates/mongoose/src/routes.ts +13 -13
- package/templates/mongoose/src/server.ts +38 -18
- package/templates/mongoose/src/swaggerOptions.ts +23 -23
- package/templates/mongoose/tests/app.test.ts +97 -0
- package/templates/mongoose/tests/global-setup.ts +15 -0
- package/templates/mongoose/tests/helpers/fixtures.ts +42 -0
- package/templates/mongoose/tests/routes/account.test.ts +614 -0
- package/templates/mongoose/tests/routes/profile.test.ts +95 -0
- package/templates/mongoose/tests/routes/social.test.ts +310 -0
- package/templates/mongoose/tests/setup.ts +25 -0
- package/templates/mongoose/tsconfig.json +16 -20
- package/templates/mongoose/vitest.config.ts +43 -0
- package/templates/prisma/.dockerignore +2 -0
- package/templates/prisma/.env.example +62 -6
- package/templates/prisma/.prettierignore +3 -0
- package/templates/prisma/.prettierrc +7 -0
- package/templates/prisma/.projectrc.json +1 -1
- package/templates/prisma/Dockerfile +21 -7
- package/templates/prisma/_gitignore +35 -0
- package/templates/prisma/docker-compose.yml +8 -7
- package/templates/prisma/eslint.config.js +35 -0
- package/templates/prisma/package-lock.json +7327 -0
- package/templates/prisma/package.json +47 -9
- package/templates/prisma/prisma/schema/account.schema.prisma +92 -10
- package/templates/prisma/prisma/schema/profile.schema.prisma +8 -3
- package/templates/prisma/prisma/schema/schema.prisma +1 -2
- package/templates/prisma/prisma.config.ts +5 -5
- package/templates/prisma/readme.md +81 -0
- package/templates/prisma/src/app/configs/index.ts +175 -26
- package/templates/prisma/src/app/errors/zodError.ts +16 -16
- package/templates/prisma/src/app/lib/logger.ts +12 -0
- package/templates/prisma/src/app/lib/prisma.ts +4 -6
- package/templates/prisma/src/app/middlewares/auth.ts +37 -16
- package/templates/prisma/src/app/middlewares/global_error_handler.ts +42 -35
- package/templates/prisma/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/prisma/src/app/middlewares/rate_limiter.ts +124 -0
- package/templates/prisma/src/app/middlewares/request_validator.ts +4 -3
- package/templates/prisma/src/app/middlewares/uploader.ts +17 -13
- package/templates/prisma/src/app/modules/account/account.controller.ts +74 -34
- package/templates/prisma/src/app/modules/account/account.route.ts +68 -22
- package/templates/prisma/src/app/modules/account/account.service.ts +329 -309
- package/templates/prisma/src/app/modules/account/account.swagger.ts +167 -76
- package/templates/prisma/src/app/modules/account/account.validation.ts +41 -19
- package/templates/prisma/src/app/modules/profile/profile.controller.ts +4 -4
- package/templates/prisma/src/app/modules/profile/profile.route.ts +18 -12
- package/templates/prisma/src/app/modules/profile/profile.service.ts +11 -18
- package/templates/prisma/src/app/modules/profile/profile.swagger.ts +11 -11
- package/templates/prisma/src/app/modules/profile/profile.validation.ts +2 -2
- package/templates/prisma/src/app/passport-js/index.ts +87 -0
- package/templates/prisma/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/prisma/src/app/passport-js/providers.ts +263 -0
- package/templates/prisma/src/app/passport-js/social-account.ts +147 -0
- package/templates/prisma/src/app/queues/boss.ts +8 -0
- package/templates/prisma/src/app/queues/email.queue.ts +56 -0
- package/templates/prisma/src/app/queues/index.ts +14 -0
- package/templates/prisma/src/app/queues/maintenance.queue.ts +30 -0
- package/templates/prisma/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/prisma/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/prisma/src/app/types/error.d.ts +7 -7
- package/templates/prisma/src/app/types/express.d.ts +11 -0
- package/templates/prisma/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/prisma/src/app/utils/JWT.ts +36 -17
- package/templates/prisma/src/app/utils/app_error.ts +10 -10
- package/templates/prisma/src/app/utils/auth_cookie.ts +27 -0
- package/templates/prisma/src/app/utils/catch_async.ts +8 -8
- package/templates/prisma/src/app/utils/cloudinary.ts +26 -59
- package/templates/prisma/src/app/utils/mail_sender.ts +26 -92
- package/templates/prisma/src/app/utils/manage_response.ts +19 -19
- package/templates/prisma/src/app/utils/otp.ts +20 -0
- package/templates/prisma/src/app/utils/session.ts +57 -0
- package/templates/prisma/src/app.ts +73 -42
- package/templates/prisma/src/routes.ts +12 -10
- package/templates/prisma/src/server.ts +37 -19
- package/templates/prisma/src/swaggerOptions.ts +22 -26
- package/templates/prisma/tests/app.test.ts +86 -0
- package/templates/prisma/tests/helpers/fixtures.ts +35 -0
- package/templates/prisma/tests/helpers/prisma.ts +32 -0
- package/templates/prisma/tests/routes/account.test.ts +627 -0
- package/templates/prisma/tests/routes/profile.test.ts +91 -0
- package/templates/prisma/tests/routes/social.test.ts +317 -0
- package/templates/prisma/tests/setup.ts +10 -0
- package/templates/prisma/tsconfig.json +16 -23
- package/templates/prisma/vitest.config.ts +38 -0
- package/bin/cli.js +0 -41
- package/src/createProject.js +0 -61
- package/src/generateModule.js +0 -93
- package/src/utils/getProjectType.js +0 -13
- package/src/utils/mongoose/controllerGenerator.js +0 -70
- package/src/utils/mongoose/routeGenerator.js +0 -27
- package/src/utils/mongoose/schemaGenerator.js +0 -11
- package/src/utils/mongoose/serviceGenerator.js +0 -49
- package/src/utils/mongoose/swaggerGenerator.js +0 -112
- package/src/utils/mongoose/validationGenerator.js +0 -13
- package/src/utils/prisma/prismaSchemaGenerator.js +0 -10
- package/src/utils/prisma/prismaServiceGenerator.js +0 -47
- package/src/utils/routeInjector.js +0 -40
- package/src/utils/swaggerInjector.js +0 -52
- package/src/utils/validateModuleName.js +0 -17
- package/src/utils/validateProjectName.js +0 -15
- package/src/utils.js +0 -26
- package/templates/mongoose/src/app/errors/castError.ts +0 -23
- package/templates/mongoose/src/app/errors/duplicateError.ts +0 -26
- package/templates/mongoose/src/app/errors/validationError.ts +0 -25
- package/templates/mongoose/src/app/modules/auth/auth.controller.ts +0 -127
- package/templates/mongoose/src/app/modules/auth/auth.interface.ts +0 -24
- package/templates/mongoose/src/app/modules/auth/auth.route.ts +0 -46
- package/templates/mongoose/src/app/modules/auth/auth.schema.ts +0 -19
- package/templates/mongoose/src/app/modules/auth/auth.service.ts +0 -324
- package/templates/mongoose/src/app/modules/auth/auth.swagger.ts +0 -214
- package/templates/mongoose/src/app/modules/auth/auth.validation.ts +0 -37
- package/templates/mongoose/src/app/modules/user/user.controller.ts +0 -17
- package/templates/mongoose/src/app/modules/user/user.interface.ts +0 -15
- package/templates/mongoose/src/app/modules/user/user.route.ts +0 -20
- package/templates/mongoose/src/app/modules/user/user.schema.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.service.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.swagger.ts +0 -75
- package/templates/mongoose/src/app/modules/user/user.validation.ts +0 -18
- package/templates/mongoose/src/app/types/index.d.ts +0 -12
- package/templates/mongoose/src/app/utils/isAccountExist.ts +0 -31
- package/templates/prisma/src/app/types/index.d.ts +0 -12
- 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": "
|
|
8
|
-
"
|
|
9
|
-
"
|
|
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": "
|
|
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
|
|
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": "^
|
|
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.
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import { configs } from '../configs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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;
|