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
@@ -1,394 +1,410 @@
1
- import bcrypt from "bcrypt";
2
- import { Request } from "express";
3
- import { configs } from "../../configs";
4
- import { prisma } from "../../lib/prisma";
5
- import { AppError } from "../../utils/app_error";
6
- import { jwtHelpers } from "../../utils/JWT";
7
- import { otpGenerator } from "../../utils/otpGenerator";
8
- import sendMail from "../../utils/mail_sender";
1
+ import { Prisma } from '@prisma/client';
2
+ import bcrypt from 'bcrypt';
3
+ import type { Request } from 'express';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { configs } from '../../configs/index.js';
6
+ import { prisma } from '../../lib/prisma.js';
7
+ import { hashLoginCode } from '../../passport-js/social-account.js';
8
+ import { enqueueEmail } from '../../queues/email.queue.js';
9
+ import { AppError } from '../../utils/app_error.js';
10
+ import { jwtHelpers, type TokenUser } from '../../utils/JWT.js';
11
+ import { hashRefreshToken, readRefreshToken } from '../../utils/session.js';
12
+ import {
13
+ generateOtp,
14
+ hashOtp,
15
+ isOtpMatch,
16
+ OTP_EXPIRY_MS,
17
+ OTP_MAX_ATTEMPTS,
18
+ OTP_RESEND_COOLDOWN_MS,
19
+ } from '../../utils/otp.js';
9
20
 
10
- const create_account_into_db = async (req: Request) => {
11
- const payload = req?.body;
21
+ // cheap hashes under tests, the real cost everywhere else
22
+ const BCRYPT_ROUNDS = configs.is_test ? 4 : 12;
12
23
 
13
- // hash password
14
- const hashPassword = bcrypt.hashSync(payload.password, 10);
24
+ // a rotated refresh token that comes back within this window is most likely a
25
+ // parallel refresh (two tabs at once), not theft, so it doesn't end the session
26
+ const REFRESH_REUSE_GRACE_MS = 30 * 1000;
15
27
 
16
- // create account and profile
17
- const result = await prisma.$transaction(async (tx) => {
18
- const account = await tx.account.create({
19
- data: {
20
- email: payload.email,
21
- password: hashPassword,
22
- },
23
- });
28
+ // a valid hash to compare against when the account doesn't exist, so login takes
29
+ // the same time either way (prevents finding registered emails by timing)
30
+ const DUMMY_HASH = bcrypt.hashSync('dummy-password', BCRYPT_ROUNDS);
24
31
 
25
- const profile = await tx.profile.create({
26
- data: {
27
- fullName: payload.fullName,
28
- accountId: account.id,
29
- },
30
- });
32
+ type VerifyTokenPayload = { purpose: 'verify-account'; accountId: string };
33
+ // `tv` is the account's token version; any password change makes the link stale
34
+ type ResetTokenPayload = { purpose: 'reset-password'; accountId: string; tv: number };
31
35
 
32
- return {
33
- account,
34
- profile,
35
- };
36
- });
36
+ const toTokenUser = (account: {
37
+ id: string;
38
+ email: string;
39
+ role: TokenUser['role'];
40
+ tokenVersion: number;
41
+ }): TokenUser => ({
42
+ accountId: account.id,
43
+ email: account.email,
44
+ role: account.role,
45
+ tokenVersion: account.tokenVersion,
46
+ });
37
47
 
38
- // sending otp and verification link
39
- const newOtp = otpGenerator();
40
- const verificationToken = jwtHelpers.generateToken(
41
- {
42
- email: payload.email,
43
- accountId: result.account.id,
44
- },
45
- configs.jwt.verified_token as string,
46
- "5m",
48
+ /** A fresh OTP + verification link, and the columns to store for it. */
49
+ const newVerification = (accountId: string) => {
50
+ const otp = generateOtp();
51
+ const token = jwtHelpers.generateToken(
52
+ { purpose: 'verify-account', accountId } satisfies VerifyTokenPayload,
53
+ configs.jwt.verified_token,
54
+ '5m',
47
55
  );
48
- const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
49
- // save otp into db
50
- await prisma.account.update({
51
- where: {
52
- email: payload.email,
53
- },
54
- data: {
55
- lastOtp: newOtp,
56
- lastOtpSendingTime: new Date(),
57
- },
58
- });
59
-
60
- await sendMail({
61
- to: payload.email as string,
62
- subject: "welcome to - Please verify your account",
63
- htmlBody: `
64
- <p><strong>OTP</strong> ${newOtp}</p>
65
- <small>Otp will be expire in 5 minutes</small>
56
+ return {
57
+ otp,
58
+ verificationLink: `${configs.jwt.front_end_url}/verify?token=${encodeURIComponent(token)}`,
59
+ data: { lastOtp: hashOtp(otp), lastOtpSendingTime: new Date(), otpAttempts: 0 },
60
+ };
61
+ };
66
62
 
67
- <br/> <br/>
63
+ const create_account_into_db = async (req: Request) => {
64
+ const payload: { email: string; password: string; fullName: string } = req.body;
68
65
 
69
- <p>Or you can use Verification link </p>
70
- <p>${verificationLink}</p>
71
- `,
72
- textBody: "You can use otp or direct link",
73
- name: payload.fullName,
66
+ const exists = await prisma.account.findUnique({
67
+ where: { email: payload.email },
68
+ select: { id: true },
74
69
  });
75
- return result;
76
- };
70
+ if (exists) {
71
+ throw new AppError('An account with this email already exists', 409);
72
+ }
77
73
 
78
- const verify_account_using_otp_into_db = async (req: Request) => {
79
- const payload: { email: string; otp: string } = req?.body;
80
- // check account
81
- const account = await prisma.account.findUnique({
82
- where: {
74
+ // id generated up front so the account and its OTP are written in one query
75
+ const id = randomUUID();
76
+ const { otp, verificationLink, data } = newVerification(id);
77
+
78
+ const account = await prisma.account.create({
79
+ data: {
80
+ id,
83
81
  email: payload.email,
82
+ password: await bcrypt.hash(payload.password, BCRYPT_ROUNDS),
83
+ ...data,
84
+ profile: { create: { fullName: payload.fullName } },
84
85
  },
86
+ select: { id: true, email: true, role: true, isAccountVerified: true, profile: true },
85
87
  });
86
- // check if account exists
87
- if (!account) {
88
- throw new AppError("Account not found", 404);
88
+
89
+ await enqueueEmail({ type: 'verify-account', email: account.email, otp, verificationLink });
90
+ return account;
91
+ };
92
+
93
+ const verify_account_using_otp_into_db = async (req: Request) => {
94
+ const payload: { email: string; otp: string } = req.body;
95
+ const invalid = new AppError('Invalid or expired OTP', 401);
96
+
97
+ const account = await prisma.account.findUnique({ where: { email: payload.email } });
98
+ if (!account || account.isAccountVerified || !account.lastOtp) {
99
+ throw invalid;
89
100
  }
90
- // match with last otp
91
- const isOtpMatch = payload.otp === account.lastOtp;
92
- if (!isOtpMatch) {
93
- throw new AppError("Invalid OTP, Please try again!!", 401);
101
+
102
+ const isExpired =
103
+ !account.lastOtpSendingTime ||
104
+ Date.now() - account.lastOtpSendingTime.getTime() > OTP_EXPIRY_MS;
105
+ if (isExpired) {
106
+ throw invalid;
94
107
  }
95
- // check otp timing
96
- const OTP_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes in ms
97
- const isOtpExpired = account.lastOtpSendingTime
98
- ? new Date().getTime() - new Date(account.lastOtpSendingTime).getTime() >
99
- OTP_EXPIRY_TIME
100
- : true;
101
-
102
- if (isOtpExpired) {
103
- throw new AppError("OTP Expired, Please try again!!", 401);
108
+
109
+ // use up an attempt atomically, so parallel guesses can't go past the limit
110
+ const attempt = await prisma.account.updateMany({
111
+ where: { id: account.id, otpAttempts: { lt: OTP_MAX_ATTEMPTS } },
112
+ data: { otpAttempts: { increment: 1 } },
113
+ });
114
+ if (!attempt.count) {
115
+ throw new AppError('Too many wrong attempts, please request a new OTP', 429);
104
116
  }
105
- // change account status
106
- await prisma.account.update({
107
- where: {
108
- id: account.id,
109
- },
110
- data: {
111
- isAccountVerified: true,
112
- },
117
+
118
+ if (!isOtpMatch(payload.otp, account.lastOtp)) {
119
+ throw invalid;
120
+ }
121
+
122
+ // only succeeds if this OTP wasn't replaced or used in the meantime
123
+ const verified = await prisma.account.updateMany({
124
+ where: { id: account.id, lastOtp: account.lastOtp },
125
+ data: { isAccountVerified: true, lastOtp: null, otpAttempts: 0 },
113
126
  });
114
- // infuter user welcome email
115
- return "";
127
+ if (!verified.count) {
128
+ throw invalid;
129
+ }
130
+ return null;
116
131
  };
117
132
 
118
133
  const verify_account_using_link_into_db = async (req: Request) => {
119
- const token = req?.body?.token as string;
120
- let decoadeToken: any;
121
- try {
122
- decoadeToken = jwtHelpers.verifyToken(
123
- token,
124
- configs.jwt.verified_token as string,
125
- );
126
- } catch (error: any) {
127
- if (error?.message == "invalid signature") {
128
- throw new AppError("Invalid Token", 403);
129
- } else if (error?.message == "jwt expired") {
130
- throw new AppError("Token expired, please reset again", 403);
131
- }
132
- }
133
- // check account
134
- const account = await prisma.account.findUnique({
135
- where: {
136
- email: decoadeToken.email,
137
- },
138
- });
139
- // check if account exists
140
- if (!account) {
141
- throw new AppError("Account not found", 404);
134
+ const decoded = jwtHelpers.verifyToken<VerifyTokenPayload>(
135
+ req.body.token,
136
+ configs.jwt.verified_token,
137
+ );
138
+ if (decoded.purpose !== 'verify-account') {
139
+ throw new AppError('Invalid token', 401);
142
140
  }
143
141
 
144
- // change account status
145
- await prisma.account.update({
146
- where: {
147
- id: account.id,
148
- },
149
- data: {
150
- isAccountVerified: true,
151
- },
142
+ const result = await prisma.account.updateMany({
143
+ where: { id: decoded.accountId, isDeleted: false },
144
+ data: { isAccountVerified: true, lastOtp: null, otpAttempts: 0 },
152
145
  });
153
- // infuter user welcome email
154
- return "";
146
+ if (!result.count) {
147
+ throw new AppError('Invalid token', 401);
148
+ }
149
+ return null;
155
150
  };
156
151
 
157
- const login_user_into_db = async (req: Request) => {
158
- const payload = req?.body;
159
- const account = await prisma.account.findUnique({
160
- where: {
161
- email: payload.email,
162
- },
163
- });
164
-
165
- // check if account exists
166
- if (!account) {
167
- throw new AppError("Account not found", 404);
168
- }
152
+ const login_user_into_db = async (req: Request): Promise<TokenUser> => {
153
+ const payload: { email: string; password: string } = req.body;
154
+ const invalid = new AppError('Invalid email or password', 401);
169
155
 
170
- // checking password
171
- const isPasswordMatch = bcrypt.compareSync(
172
- payload.password,
173
- account.password,
174
- );
156
+ const account = await prisma.account.findUnique({ where: { email: payload.email } });
175
157
 
176
- if (!isPasswordMatch) {
177
- throw new AppError("Invalid password", 401);
158
+ // always run bcrypt, and never skip the password check: social-login accounts
159
+ // have no password and must sign in through their provider
160
+ const isPasswordMatch = await bcrypt.compare(payload.password, account?.password ?? DUMMY_HASH);
161
+ if (!account || !account.password || !isPasswordMatch) {
162
+ throw invalid;
178
163
  }
179
164
 
180
- // check if account is deleted
181
165
  if (account.isDeleted) {
182
- throw new AppError("Account is deleted", 401);
166
+ throw new AppError('Account is deleted', 401);
183
167
  }
184
- // check if account is verified
185
168
  if (!account.isAccountVerified) {
186
- throw new AppError("Account is not verified", 401);
169
+ throw new AppError('Account is not verified', 401);
187
170
  }
188
171
 
189
- // generate access
190
- const accessToken = jwtHelpers.generateToken(
191
- {
192
- email: account.email,
193
- role: account.role,
194
- accountId: account.id,
195
- },
196
- configs.jwt.access_token as string,
197
- configs.jwt.access_expires as string,
198
- );
199
- return accessToken;
172
+ return toTokenUser(account);
200
173
  };
201
174
 
202
175
  const get_user_account_from_db = async (req: Request) => {
203
- const user = req?.user;
204
-
205
- const result = await prisma.account.findUnique({
206
- where: {
207
- id: user?.accountId,
208
- },
176
+ return prisma.account.findUnique({
177
+ where: { id: req.user!.accountId },
209
178
  select: {
210
179
  id: true,
211
180
  email: true,
212
181
  role: true,
182
+ provider: true,
213
183
  isAccountVerified: true,
214
184
  isDeleted: true,
215
185
  profile: true,
186
+ socialAccounts: { select: { provider: true, createdAt: true } },
216
187
  },
217
188
  });
218
- return result;
219
189
  };
220
190
 
221
- const change_password_into_db = async (req: Request) => {
222
- const user = req?.user;
223
- // payload
224
- const payload: { oldPassword: string; newPassword: string } = req?.body;
225
- // check old and new password is not same
226
- const isSamePassword = payload.oldPassword === payload.newPassword;
227
- if (isSamePassword) {
228
- throw new AppError(
229
- "Old and new password are same, Please provide deffirent password",
230
- 404,
231
- );
191
+ /** Changes the password and revokes every other session. Returns the user for a new token. */
192
+ const change_password_into_db = async (req: Request): Promise<TokenUser> => {
193
+ const payload: { oldPassword: string; newPassword: string } = req.body;
194
+
195
+ if (payload.oldPassword === payload.newPassword) {
196
+ throw new AppError('New password must be different from the old password', 400);
232
197
  }
233
- // check user validity
234
- const isUserExist = await prisma.account.findFirst({
235
- where: {
236
- email: user?.email,
237
- },
238
- });
239
- // if account not exists
240
- if (!isUserExist) {
241
- throw new AppError("Account not found!!", 404);
198
+
199
+ const account = await prisma.account.findUnique({ where: { id: req.user!.accountId } });
200
+ if (!account) {
201
+ throw new AppError('Account not found', 404);
242
202
  }
243
- // check old password
244
- const isPasswordMatch = bcrypt.compareSync(
245
- payload.oldPassword,
246
- isUserExist.password,
247
- );
203
+ if (!account.password) {
204
+ throw new AppError('This account uses social login and has no password', 400);
205
+ }
206
+
207
+ const isPasswordMatch = await bcrypt.compare(payload.oldPassword, account.password);
248
208
  if (!isPasswordMatch) {
249
- throw new AppError("Incorrect password", 401);
209
+ throw new AppError('Incorrect password', 401);
250
210
  }
251
- // change password logic
252
- const newHashPassword = bcrypt.hashSync(payload.newPassword, 10);
253
- await prisma.account.update({
254
- where: {
255
- id: isUserExist.id,
256
- },
211
+
212
+ const updated = await prisma.account.update({
213
+ where: { id: account.id },
257
214
  data: {
258
- password: newHashPassword,
215
+ password: await bcrypt.hash(payload.newPassword, BCRYPT_ROUNDS),
216
+ tokenVersion: { increment: 1 },
259
217
  },
260
218
  });
261
-
262
- // in future email notification for more sucurity
263
- return "";
219
+ await prisma.refreshToken.deleteMany({ where: { accountId: account.id } });
220
+ return toTokenUser(updated);
264
221
  };
265
222
 
266
223
  const resend_otp_and_verification_link_from_db = async (req: Request) => {
267
- const email = req?.body?.email as string;
224
+ const email: string = req.body.email;
268
225
  const account = await prisma.account.findUnique({
269
- where: {
270
- email,
271
- },
272
- });
273
- // check if account exists
274
- if (!account) {
275
- throw new AppError("Account not found", 404);
276
- }
277
- // if already verified
278
- if (account.isAccountVerified) {
279
- throw new AppError("Account already verified", 403);
280
- }
281
- // make new otp and verification link
282
- const newOtp = otpGenerator();
283
- const verificationToken = jwtHelpers.generateToken(
284
- {
285
- email,
286
- accountId: account.id,
287
- },
288
- configs.jwt.verified_token as string,
289
- "5m",
290
- );
291
- const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
292
- // save otp into db
293
- await prisma.account.update({
294
- where: {
295
- email,
296
- },
297
- data: {
298
- lastOtp: newOtp,
299
- lastOtpSendingTime: new Date(),
300
- },
226
+ where: { email },
227
+ select: { id: true, isAccountVerified: true },
301
228
  });
302
229
 
303
- await sendMail({
304
- to: email as string,
305
- subject: "New Verification otp and link",
306
- htmlBody: `
307
- <p><strong>OTP</strong> ${newOtp}</p>
308
- <small>Otp will be expire in 5 minutes</small>
230
+ // same response whether or not the account exists (no email enumeration)
231
+ if (!account || account.isAccountVerified) return null;
309
232
 
310
- <br/> <br/>
233
+ const { otp, verificationLink, data } = newVerification(account.id);
311
234
 
312
- <p>Or you can use Verification link </p>
313
- <p>${verificationLink}</p>
314
- `,
315
- textBody: "You can use otp or direct link",
235
+ // the cooldown check is part of the update, so parallel requests send one email
236
+ const result = await prisma.account.updateMany({
237
+ where: {
238
+ id: account.id,
239
+ isAccountVerified: false,
240
+ OR: [
241
+ { lastOtpSendingTime: null },
242
+ { lastOtpSendingTime: { lt: new Date(Date.now() - OTP_RESEND_COOLDOWN_MS) } },
243
+ ],
244
+ },
245
+ data,
316
246
  });
247
+ if (!result.count) {
248
+ throw new AppError('Please wait a minute before requesting a new OTP', 429);
249
+ }
250
+
251
+ await enqueueEmail({ type: 'verify-account', email, otp, verificationLink });
252
+ return null;
317
253
  };
318
254
 
319
- const forget_password_genereate_reset_token_from_db = async (req: Request) => {
320
- const email = req?.body?.email as string;
255
+ const forget_password_generate_reset_token_from_db = async (req: Request) => {
256
+ const email: string = req.body.email;
321
257
  const account = await prisma.account.findUnique({
322
- where: {
323
- email,
324
- },
258
+ where: { email },
259
+ select: { id: true, isDeleted: true, tokenVersion: true },
325
260
  });
326
- // check if account exists
327
- if (!account) {
328
- throw new AppError("Account not found", 404);
329
- }
330
- // generate forget token
331
- const verificationToken = jwtHelpers.generateToken(
261
+
262
+ // same response whether or not the account exists (no email enumeration)
263
+ if (!account || account.isDeleted) return null;
264
+
265
+ const resetToken = jwtHelpers.generateToken(
332
266
  {
333
- email: email,
267
+ purpose: 'reset-password',
334
268
  accountId: account.id,
335
- },
336
- configs.jwt.verified_token as string,
337
- "5m",
269
+ tv: account.tokenVersion,
270
+ } satisfies ResetTokenPayload,
271
+ configs.jwt.reset_secret,
272
+ configs.jwt.reset_expires,
338
273
  );
339
- const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
340
-
341
- await sendMail({
342
- to: email as string,
343
- subject: "Forget Password- Use this link for new password ",
344
- htmlBody: `
345
- <p>Your Reset Link: </p>
346
- <p>${verificationLink}</p>
347
- `,
348
- textBody: "",
349
- });
274
+ const resetLink = `${configs.jwt.front_end_url}/reset-password?token=${encodeURIComponent(resetToken)}`;
275
+
276
+ await enqueueEmail({ type: 'reset-password', email, resetLink });
277
+ return null;
350
278
  };
351
279
 
352
280
  const reset_password_using_token_into_db = async (req: Request) => {
353
- const token = req?.body?.token as string;
354
- const newPass = req?.body?.newPass as string;
355
- let decoadeToken: any;
356
- try {
357
- decoadeToken = jwtHelpers.verifyToken(
358
- token,
359
- configs.jwt.verified_token as string,
360
- );
361
- } catch (error: any) {
362
- if (error?.message == "invalid signature") {
363
- throw new AppError("Invalid Token", 403);
364
- } else if (error?.message == "jwt expired") {
365
- throw new AppError("Link expired, please reset again", 403);
366
- }
281
+ const { token, newPass }: { token: string; newPass: string } = req.body;
282
+ const invalid = new AppError('Invalid or expired reset link', 401);
283
+
284
+ const decoded = jwtHelpers.verifyToken<ResetTokenPayload>(token, configs.jwt.reset_secret);
285
+ if (decoded.purpose !== 'reset-password' || typeof decoded.tv !== 'number') {
286
+ throw invalid;
367
287
  }
368
- // check account
369
- const account = await prisma.account.findUnique({
370
- where: {
371
- email: decoadeToken.email,
288
+
289
+ // Matching on the token version makes the link single-use (the update bumps it)
290
+ // and also signs out every existing session. Following the emailed link proves
291
+ // ownership of the address, so the account counts as verified from now on.
292
+ const result = await prisma.account.updateMany({
293
+ where: { id: decoded.accountId, isDeleted: false, tokenVersion: decoded.tv },
294
+ data: {
295
+ password: await bcrypt.hash(newPass, BCRYPT_ROUNDS),
296
+ tokenVersion: { increment: 1 },
297
+ isAccountVerified: true,
298
+ lastOtp: null,
299
+ otpAttempts: 0,
372
300
  },
373
301
  });
374
- // check if account exists
375
- if (!account) {
376
- throw new AppError("Account not found", 404);
302
+ if (!result.count) {
303
+ throw invalid;
377
304
  }
305
+ await prisma.refreshToken.deleteMany({ where: { accountId: decoded.accountId } });
306
+ return null;
307
+ };
378
308
 
379
- // change account password
380
- const newHash = bcrypt.hashSync(newPass, 10);
309
+ /** Revokes every access and refresh token issued for the account. */
310
+ const sign_out_everywhere_from_db = async (req: Request) => {
311
+ const accountId = req.user!.accountId;
381
312
  await prisma.account.update({
382
- where: {
383
- id: account.id,
384
- },
385
- data: {
386
- password: newHash,
313
+ where: { id: accountId },
314
+ data: { tokenVersion: { increment: 1 } },
315
+ });
316
+ await prisma.refreshToken.deleteMany({ where: { accountId } });
317
+ return null;
318
+ };
319
+
320
+ /** Ends the session the refresh token belongs to (this device only). */
321
+ const sign_out_from_db = async (req: Request) => {
322
+ const token = readRefreshToken(req);
323
+ if (!token) return null;
324
+ const row = await prisma.refreshToken.findUnique({
325
+ where: { tokenHash: hashRefreshToken(token) },
326
+ select: { familyId: true },
327
+ });
328
+ if (row) await prisma.refreshToken.deleteMany({ where: { familyId: row.familyId } });
329
+ return null;
330
+ };
331
+
332
+ /**
333
+ * Validates and uses up a refresh token. Returns the user and the session it
334
+ * belongs to, so the controller can issue the next token of that session.
335
+ */
336
+ const refresh_session_from_db = async (
337
+ req: Request,
338
+ ): Promise<{ user: TokenUser; familyId: string }> => {
339
+ const invalid = new AppError('Invalid or expired refresh token', 401);
340
+ const token = readRefreshToken(req);
341
+ if (!token) throw invalid;
342
+
343
+ const tokenHash = hashRefreshToken(token);
344
+ const row = await prisma.refreshToken.findUnique({
345
+ where: { tokenHash },
346
+ include: {
347
+ account: {
348
+ select: { id: true, email: true, role: true, tokenVersion: true, isDeleted: true },
349
+ },
387
350
  },
388
351
  });
389
- // infuter user alart for changing password
390
- return "";
352
+ if (!row) throw invalid;
353
+
354
+ const endSession = () => prisma.refreshToken.deleteMany({ where: { familyId: row.familyId } });
355
+
356
+ if (row.usedAt) {
357
+ // An already rotated token came back. Outside the grace window that means two
358
+ // parties hold this session (the token was stolen), so end the whole session.
359
+ if (Date.now() - row.usedAt.getTime() > REFRESH_REUSE_GRACE_MS) await endSession();
360
+ throw invalid;
361
+ }
362
+
363
+ const isStale =
364
+ row.expiresAt.getTime() < Date.now() ||
365
+ row.account.isDeleted ||
366
+ row.account.tokenVersion !== row.tokenVersion; // password changed / signed out everywhere
367
+ if (isStale) {
368
+ await endSession();
369
+ throw invalid;
370
+ }
371
+
372
+ // mark it used atomically, so of two parallel refreshes only one gets a new token
373
+ const used = await prisma.refreshToken.updateMany({
374
+ where: { tokenHash, usedAt: null },
375
+ data: { usedAt: new Date() },
376
+ });
377
+ if (!used.count) throw invalid;
378
+
379
+ return { user: toTokenUser(row.account), familyId: row.familyId };
391
380
  };
381
+
382
+ /** Trades the single-use code from a social login redirect for the user to sign in. */
383
+ const exchange_social_login_code_from_db = async (req: Request): Promise<TokenUser> => {
384
+ const invalid = new AppError('Invalid or expired login code', 401);
385
+
386
+ // deleting is what makes the code single-use, even under concurrent requests
387
+ const row = await prisma.socialLoginCode
388
+ .delete({
389
+ where: { codeHash: hashLoginCode(req.body.code) },
390
+ select: {
391
+ expiresAt: true,
392
+ account: {
393
+ select: { id: true, email: true, role: true, tokenVersion: true, isDeleted: true },
394
+ },
395
+ },
396
+ })
397
+ .catch((err) => {
398
+ if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2025') return null;
399
+ throw err;
400
+ });
401
+
402
+ if (!row || row.expiresAt.getTime() < Date.now() || row.account.isDeleted) {
403
+ throw invalid;
404
+ }
405
+ return toTokenUser(row.account);
406
+ };
407
+
392
408
  export const account_services = {
393
409
  create_account_into_db,
394
410
  login_user_into_db,
@@ -397,6 +413,10 @@ export const account_services = {
397
413
  verify_account_using_otp_into_db,
398
414
  resend_otp_and_verification_link_from_db,
399
415
  verify_account_using_link_into_db,
400
- forget_password_genereate_reset_token_from_db,
401
- reset_password_using_token_into_db
416
+ forget_password_generate_reset_token_from_db,
417
+ reset_password_using_token_into_db,
418
+ sign_out_everywhere_from_db,
419
+ sign_out_from_db,
420
+ refresh_session_from_db,
421
+ exchange_social_login_code_from_db,
402
422
  };