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
@@ -0,0 +1,278 @@
1
+ export const accountSwaggerDocs = {
2
+ '/api/auth/sign-up': {
3
+ post: {
4
+ tags: ['auth'],
5
+ summary: 'Create new account',
6
+ description: '',
7
+ requestBody: {
8
+ required: true,
9
+ content: {
10
+ 'application/json': {
11
+ example: JSON.stringify({
12
+ email: 'user@gmail.com',
13
+ password: 'password',
14
+ fullName: 'User',
15
+ }),
16
+ },
17
+ },
18
+ },
19
+ responses: {
20
+ 201: { description: 'account created successfully' },
21
+ 500: { description: 'Validation error or internal server error' },
22
+ },
23
+ },
24
+ },
25
+ '/api/auth/sign-in': {
26
+ post: {
27
+ tags: ['auth'],
28
+ summary: 'Sign In your account',
29
+ description: '',
30
+ requestBody: {
31
+ required: true,
32
+ content: {
33
+ 'application/json': {
34
+ example: JSON.stringify({
35
+ email: 'user@gmail.com',
36
+ password: 'password',
37
+ }),
38
+ },
39
+ },
40
+ },
41
+ responses: {
42
+ 201: { description: 'User signed in successfully' },
43
+ 500: { description: 'Validation error or internal server error' },
44
+ },
45
+ },
46
+ },
47
+ '/api/auth/verify-otp': {
48
+ put: {
49
+ tags: ['auth'],
50
+ summary: 'Verify OTP',
51
+ description: '',
52
+ requestBody: {
53
+ required: true,
54
+ content: {
55
+ 'application/json': {
56
+ example: JSON.stringify({
57
+ email: 'user@gmail.com',
58
+ otp: '654321',
59
+ }),
60
+ },
61
+ },
62
+ },
63
+ responses: {
64
+ 201: { description: 'OTP verification successfully' },
65
+ 500: { description: 'Validation error or internal server error' },
66
+ },
67
+ },
68
+ },
69
+ '/api/auth/verify-link': {
70
+ put: {
71
+ tags: ['auth'],
72
+ summary: 'Verify Link',
73
+ description: '',
74
+ requestBody: {
75
+ required: true,
76
+ content: {
77
+ 'application/json': {
78
+ example: JSON.stringify({
79
+ token: 'dsakfjasdkj',
80
+ }),
81
+ },
82
+ },
83
+ },
84
+ responses: {
85
+ 201: { description: 'Token verification successfully' },
86
+ 500: { description: 'Validation error or internal server error' },
87
+ },
88
+ },
89
+ },
90
+ '/api/auth/me': {
91
+ get: {
92
+ tags: ['auth'],
93
+ summary: 'Get me account',
94
+ description: '',
95
+ responses: {
96
+ 200: { description: 'account fetched successfully' },
97
+ 401: { description: 'unauthorized' },
98
+ },
99
+ },
100
+ },
101
+ '/api/auth/change-password': {
102
+ put: {
103
+ tags: ['auth'],
104
+ summary: 'Change Password',
105
+ description: '',
106
+ requestBody: {
107
+ required: true,
108
+ content: {
109
+ 'application/json': {
110
+ example: JSON.stringify({
111
+ oldPassword: '123456',
112
+ newPassword: '654321',
113
+ }),
114
+ },
115
+ },
116
+ },
117
+ responses: {
118
+ 201: { description: 'Passwrod change successfully' },
119
+ 500: { description: 'Validation error or internal server error' },
120
+ },
121
+ },
122
+ },
123
+ '/api/auth/resend-otp': {
124
+ put: {
125
+ tags: ['auth'],
126
+ summary: 'Resend OTP',
127
+ description: '',
128
+ requestBody: {
129
+ required: true,
130
+ content: {
131
+ 'application/json': {
132
+ example: JSON.stringify({
133
+ email: 'user@gmail.com',
134
+ }),
135
+ },
136
+ },
137
+ },
138
+ responses: {
139
+ 201: { description: 'OTP resend successfully' },
140
+ 500: { description: 'Validation error or internal server error' },
141
+ },
142
+ },
143
+ },
144
+ '/api/auth/forget-password': {
145
+ put: {
146
+ tags: ['auth'],
147
+ summary: 'Forget Password',
148
+ description: '',
149
+ requestBody: {
150
+ required: true,
151
+ content: {
152
+ 'application/json': {
153
+ example: JSON.stringify({
154
+ email: 'user@gmail.com',
155
+ }),
156
+ },
157
+ },
158
+ },
159
+ responses: {
160
+ 201: { description: 'Forget password successfully' },
161
+ 500: { description: 'Validation error or internal server error' },
162
+ },
163
+ },
164
+ },
165
+ '/api/auth/reset-password': {
166
+ put: {
167
+ tags: ['auth'],
168
+ summary: 'Reset Password',
169
+ description: '',
170
+ requestBody: {
171
+ required: true,
172
+ content: {
173
+ 'application/json': {
174
+ example: JSON.stringify({
175
+ token: 'dkfjadskfds',
176
+ newPass: 'newpass',
177
+ }),
178
+ },
179
+ },
180
+ },
181
+ responses: {
182
+ 201: { description: 'Password reset successfully' },
183
+ 500: { description: 'Validation error or internal server error' },
184
+ },
185
+ },
186
+ },
187
+ '/api/auth/refresh-token': {
188
+ post: {
189
+ tags: ['auth'],
190
+ summary: 'Get a new access token (and a rotated refresh token)',
191
+ description:
192
+ 'Browsers send the `refresh_token` cookie automatically; other clients send it in the body. ' +
193
+ 'Every call returns a new refresh token and invalidates the old one. Reusing an old ' +
194
+ 'refresh token ends the whole session.',
195
+ requestBody: {
196
+ required: false,
197
+ content: {
198
+ 'application/json': {
199
+ example: JSON.stringify({ refreshToken: 'only-for-non-browser-clients' }),
200
+ },
201
+ },
202
+ },
203
+ responses: {
204
+ 200: { description: '{ accessToken, refreshToken }, both also set as httpOnly cookies' },
205
+ 401: { description: 'Missing, invalid, expired or reused refresh token' },
206
+ },
207
+ },
208
+ },
209
+ '/api/auth/sign-out': {
210
+ post: {
211
+ tags: ['auth'],
212
+ summary: 'Sign out this device (revokes its refresh token, clears the cookies)',
213
+ responses: { 200: { description: 'Signed out' } },
214
+ },
215
+ },
216
+ '/api/auth/sign-out-all': {
217
+ post: {
218
+ tags: ['auth'],
219
+ summary: 'Sign out from all devices (revokes every access token)',
220
+ responses: {
221
+ 200: { description: 'Signed out everywhere' },
222
+ 401: { description: 'unauthorized' },
223
+ },
224
+ },
225
+ },
226
+ '/api/auth/social/providers': {
227
+ get: {
228
+ tags: ['social auth'],
229
+ summary: 'List the enabled social login providers',
230
+ responses: { 200: { description: 'Providers, with the URL that starts each login' } },
231
+ },
232
+ },
233
+ '/api/auth/social/{provider}': {
234
+ get: {
235
+ tags: ['social auth'],
236
+ summary: 'Start a social login (open in the browser, not via fetch)',
237
+ description:
238
+ "Link to it from the front-end: <a href='/api/auth/social/google?returnTo=/dashboard'>Login with Google</a>. " +
239
+ 'After the provider, the browser is redirected to SOCIAL_REDIRECT_URL with `?code=` (and `returnTo`) on success, ' +
240
+ 'or `?error=` (access_denied, invalid_state, email_unverified, account_disabled, social_login_failed).',
241
+ parameters: [
242
+ {
243
+ name: 'provider',
244
+ in: 'path',
245
+ required: true,
246
+ schema: {
247
+ type: 'string',
248
+ enum: ['google', 'facebook', 'github', 'twitter', 'linkedin', 'discord'],
249
+ },
250
+ },
251
+ {
252
+ name: 'returnTo',
253
+ in: 'query',
254
+ required: false,
255
+ schema: { type: 'string', example: '/dashboard' },
256
+ description: 'Path on the front-end to come back to. Only same-site paths are kept.',
257
+ },
258
+ ],
259
+ responses: { 302: { description: 'Redirect to the provider' } },
260
+ },
261
+ },
262
+ '/api/auth/social/exchange': {
263
+ post: {
264
+ tags: ['social auth'],
265
+ summary: 'Trade the single-use code from the social login redirect for an access token',
266
+ requestBody: {
267
+ required: true,
268
+ content: {
269
+ 'application/json': { example: JSON.stringify({ code: 'code-from-redirect' }) },
270
+ },
271
+ },
272
+ responses: {
273
+ 200: { description: 'Logged in (token in body and httpOnly cookie)' },
274
+ 401: { description: 'Invalid, expired or already used code' },
275
+ },
276
+ },
277
+ },
278
+ };
@@ -0,0 +1,63 @@
1
+ import z from 'zod';
2
+
3
+ const email = z.email('A valid email is required.').transform((e) => e.toLowerCase());
4
+ const password = z
5
+ .string('Password is required.')
6
+ .min(8, 'Password must be at least 8 characters.')
7
+ .max(72, 'Password must be at most 72 characters.'); // bcrypt ignores bytes past 72
8
+
9
+ const sign_up = z.object({
10
+ email,
11
+ password,
12
+ fullName: z.string('Full name is required.').trim().min(1).max(100),
13
+ });
14
+
15
+ const sign_in = z.object({
16
+ email,
17
+ password: z.string('Password is required.').max(72),
18
+ });
19
+
20
+ const change_password = z.object({
21
+ oldPassword: z.string('Old Password is required').max(72),
22
+ newPassword: password,
23
+ });
24
+
25
+ const verify_otp = z.object({
26
+ email,
27
+ otp: z.string('OTP is required').regex(/^\d{6}$/, 'OTP must be 6 digits'),
28
+ });
29
+
30
+ const token = z.string('Token is required').min(1).max(2048);
31
+
32
+ const verify_link = z.object({
33
+ token,
34
+ });
35
+
36
+ const email_only = z.object({
37
+ email,
38
+ });
39
+
40
+ const reset_pass = z.object({
41
+ token,
42
+ newPass: password,
43
+ });
44
+
45
+ // optional: browsers send the refresh token as a cookie instead
46
+ const refresh_token = z.object({ refreshToken: z.string().min(1).max(256).optional() }).optional();
47
+
48
+ const social_exchange = z.object({
49
+ code: z.string('Code is required').min(1).max(256),
50
+ });
51
+
52
+ export const account_validation = {
53
+ sign_up,
54
+ sign_in,
55
+ change_password,
56
+ verify_otp,
57
+ resend_otp: email_only,
58
+ forget_password: email_only,
59
+ verify_link,
60
+ reset_pass,
61
+ social_exchange,
62
+ refresh_token,
63
+ };
@@ -0,0 +1,17 @@
1
+ import catchAsync from '../../utils/catch_async.js';
2
+ import manageResponse from '../../utils/manage_response.js';
3
+ import { profile_service } from './profile.service.js';
4
+
5
+ const update_profile = catchAsync(async (req, res) => {
6
+ const result = await profile_service.update_profile_into_db(req);
7
+ manageResponse(res, {
8
+ success: true,
9
+ statusCode: 200,
10
+ message: 'profile updated successfully.',
11
+ data: result,
12
+ });
13
+ });
14
+
15
+ export const profile_controller = {
16
+ update_profile,
17
+ };
@@ -0,0 +1,7 @@
1
+ import type { Types } from 'mongoose';
2
+
3
+ export type T_profile = {
4
+ accountId: Types.ObjectId;
5
+ fullName: string;
6
+ profilePhoto: string | null;
7
+ };
@@ -0,0 +1,28 @@
1
+ import { Router } from 'express';
2
+ import auth from '../../middlewares/auth.js';
3
+ import RequestValidator from '../../middlewares/request_validator.js';
4
+ import uploader from '../../middlewares/uploader.js';
5
+ import { AppError } from '../../utils/app_error.js';
6
+ import { profile_controller } from './profile.controller.js';
7
+ import { profile_validations } from './profile.validation.js';
8
+
9
+ const router = Router();
10
+
11
+ router.patch(
12
+ '/',
13
+ auth(),
14
+ uploader.single('file'),
15
+ // multipart forms send the JSON fields as a `data` string
16
+ (req, _res, next) => {
17
+ try {
18
+ req.body = req.body?.data ? JSON.parse(req.body.data) : {};
19
+ next();
20
+ } catch {
21
+ next(new AppError('`data` must be valid JSON', 400));
22
+ }
23
+ },
24
+ RequestValidator(profile_validations.update_profile),
25
+ profile_controller.update_profile,
26
+ );
27
+
28
+ export default router;
@@ -0,0 +1,13 @@
1
+ import { model, Schema } from 'mongoose';
2
+ import type { T_profile } from './profile.interface.js';
3
+
4
+ const profileSchema = new Schema<T_profile>(
5
+ {
6
+ accountId: { type: Schema.Types.ObjectId, ref: 'account', required: true, unique: true },
7
+ fullName: { type: String, required: true, trim: true },
8
+ profilePhoto: { type: String, default: null },
9
+ },
10
+ { timestamps: true, versionKey: false },
11
+ );
12
+
13
+ export const profile_model = model('profile', profileSchema);
@@ -0,0 +1,28 @@
1
+ import { Request } from 'express';
2
+ import uploadCloud from '../../utils/cloudinary.js';
3
+ import { profile_model } from './profile.schema.js';
4
+
5
+ const update_profile_into_db = async (req: Request) => {
6
+ const payload: { fullName?: string } = req.body;
7
+
8
+ // validation already ran (see profile.route.ts), so only valid requests upload
9
+ const profilePhoto = req.file ? (await uploadCloud(req.file, 'profiles')).secure_url : undefined;
10
+
11
+ // only the known fields, never the raw body (no operator or field injection)
12
+ const update = {
13
+ ...(payload.fullName !== undefined && { fullName: payload.fullName }),
14
+ ...(profilePhoto !== undefined && { profilePhoto }),
15
+ };
16
+
17
+ return profile_model
18
+ .findOneAndUpdate(
19
+ { accountId: req.user!.accountId },
20
+ { $set: update },
21
+ { returnDocument: 'after' },
22
+ )
23
+ .lean();
24
+ };
25
+
26
+ export const profile_service = {
27
+ update_profile_into_db,
28
+ };
@@ -0,0 +1,34 @@
1
+ export const profileSwaggerDocs = {
2
+ '/api/profile': {
3
+ patch: {
4
+ tags: ['profile'],
5
+ summary: 'Update profile',
6
+ requestBody: {
7
+ required: true,
8
+ content: {
9
+ 'multipart/form-data': {
10
+ schema: {
11
+ type: 'object',
12
+ properties: {
13
+ data: {
14
+ type: 'object',
15
+ properties: {
16
+ fullName: { type: 'string' },
17
+ },
18
+ },
19
+ file: {
20
+ type: 'string',
21
+ format: 'binary',
22
+ },
23
+ },
24
+ },
25
+ },
26
+ },
27
+ },
28
+ responses: {
29
+ 200: { description: 'profile updated successfully' },
30
+ 500: { description: 'Validation error or internal server error' },
31
+ },
32
+ },
33
+ },
34
+ };
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+ const update_profile = z.object({
3
+ fullName: z.string().trim().min(1).max(100).optional(),
4
+ });
5
+
6
+ export const profile_validations = {
7
+ update_profile,
8
+ };
@@ -0,0 +1,87 @@
1
+ import type { Express, NextFunction, Request, Response } from 'express';
2
+ import { Router } from 'express';
3
+ import passport from 'passport';
4
+ import { configs } from '../configs/index.js';
5
+ import { logger } from '../lib/logger.js';
6
+ import { rateLimiters } from '../middlewares/rate_limiter.js';
7
+ import manageResponse from '../utils/manage_response.js';
8
+ import { getReturnTo } from './oauth-state.js';
9
+ import { enabledSocialProviders, type SocialProvider } from './providers.js';
10
+ import { createSocialLoginCode, SocialLoginError } from './social-account.js';
11
+
12
+ for (const provider of enabledSocialProviders) {
13
+ passport.use(provider.name, provider.createStrategy());
14
+ }
15
+
16
+ /** Mounts passport on the app. Logins are stateless, no session is needed. */
17
+ export const setupPassport = (app: Express) => {
18
+ app.use(passport.initialize());
19
+ };
20
+
21
+ /** Sends the browser back to the front-end with `?code=` or `?error=`. */
22
+ const redirectToFrontEnd = (res: Response, params: Record<string, string | undefined>) => {
23
+ const url = new URL(configs.social.redirect_url);
24
+ for (const [key, value] of Object.entries(params)) {
25
+ if (value) url.searchParams.set(key, value);
26
+ }
27
+ res.redirect(url.toString());
28
+ };
29
+
30
+ /** Maps a failed handshake to a short error code the front-end can show. */
31
+ const errorCode = (req: Request, err: unknown, info: unknown): string => {
32
+ if (err instanceof SocialLoginError) return err.code;
33
+ if (req.query.error === 'access_denied' || req.query.denied) return 'access_denied';
34
+ const message = err instanceof Error ? err.message : (info as { message?: string })?.message;
35
+ if (message === 'invalid_state') return 'invalid_state';
36
+ if (err) logger.error({ err }, 'Social login failed');
37
+ return 'social_login_failed';
38
+ };
39
+
40
+ const callback =
41
+ ({ name }: SocialProvider) =>
42
+ (req: Request, res: Response, next: NextFunction) => {
43
+ passport.authenticate(
44
+ name,
45
+ { session: false },
46
+ async (err: unknown, user: Express.User | false, info: unknown) => {
47
+ const returnTo = getReturnTo(res);
48
+ if (err || !user) {
49
+ return redirectToFrontEnd(res, { error: errorCode(req, err, info), returnTo });
50
+ }
51
+ try {
52
+ const code = await createSocialLoginCode(user.accountId);
53
+ redirectToFrontEnd(res, { code, returnTo });
54
+ } catch (e) {
55
+ next(e);
56
+ }
57
+ },
58
+ )(req, res, next);
59
+ };
60
+
61
+ /**
62
+ * `GET /providers` lists the enabled providers (for rendering login buttons),
63
+ * `GET /<provider>?returnTo=/path` starts a login, `GET /<provider>/callback` finishes it.
64
+ */
65
+ export const socialAuthRouter = Router();
66
+
67
+ socialAuthRouter.get('/providers', (_req, res) => {
68
+ manageResponse(res, {
69
+ statusCode: 200,
70
+ success: true,
71
+ message: 'Social login providers fetched successfully',
72
+ data: enabledSocialProviders.map(({ name, label }) => ({
73
+ name,
74
+ label,
75
+ url: `/api/auth/social/${name}`,
76
+ })),
77
+ });
78
+ });
79
+
80
+ for (const provider of enabledSocialProviders) {
81
+ socialAuthRouter.get(
82
+ `/${provider.name}`,
83
+ rateLimiters.social,
84
+ passport.authenticate(provider.name, { session: false }),
85
+ );
86
+ socialAuthRouter.get(`/${provider.name}/callback`, callback(provider));
87
+ }