@flowerforce/flowerbase 1.2.0 → 1.2.1-beta.11

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 (125) hide show
  1. package/README.md +28 -3
  2. package/dist/auth/controller.d.ts.map +1 -1
  3. package/dist/auth/controller.js +57 -3
  4. package/dist/auth/plugins/jwt.d.ts.map +1 -1
  5. package/dist/auth/plugins/jwt.js +49 -3
  6. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  7. package/dist/auth/providers/custom-function/controller.js +19 -3
  8. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  9. package/dist/auth/providers/local-userpass/controller.js +125 -71
  10. package/dist/auth/providers/local-userpass/dtos.d.ts +11 -2
  11. package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
  12. package/dist/auth/utils.d.ts +53 -14
  13. package/dist/auth/utils.d.ts.map +1 -1
  14. package/dist/auth/utils.js +46 -63
  15. package/dist/constants.d.ts +14 -0
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/constants.js +18 -5
  18. package/dist/features/functions/controller.d.ts.map +1 -1
  19. package/dist/features/functions/controller.js +32 -3
  20. package/dist/features/functions/dtos.d.ts +3 -0
  21. package/dist/features/functions/dtos.d.ts.map +1 -1
  22. package/dist/features/functions/interface.d.ts +3 -0
  23. package/dist/features/functions/interface.d.ts.map +1 -1
  24. package/dist/features/functions/utils.d.ts +2 -1
  25. package/dist/features/functions/utils.d.ts.map +1 -1
  26. package/dist/features/functions/utils.js +19 -7
  27. package/dist/features/rules/utils.d.ts.map +1 -1
  28. package/dist/features/rules/utils.js +11 -2
  29. package/dist/features/triggers/index.d.ts.map +1 -1
  30. package/dist/features/triggers/index.js +48 -7
  31. package/dist/features/triggers/utils.d.ts.map +1 -1
  32. package/dist/features/triggers/utils.js +118 -27
  33. package/dist/index.d.ts +8 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +57 -21
  36. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  37. package/dist/services/mongodb-atlas/index.js +605 -478
  38. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  39. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  40. package/dist/services/mongodb-atlas/utils.d.ts +9 -2
  41. package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
  42. package/dist/services/mongodb-atlas/utils.js +113 -23
  43. package/dist/shared/handleUserRegistration.d.ts.map +1 -1
  44. package/dist/shared/handleUserRegistration.js +4 -1
  45. package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
  46. package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
  47. package/dist/utils/context/helpers.d.ts +7 -6
  48. package/dist/utils/context/helpers.d.ts.map +1 -1
  49. package/dist/utils/context/helpers.js +3 -0
  50. package/dist/utils/context/index.d.ts +1 -1
  51. package/dist/utils/context/index.d.ts.map +1 -1
  52. package/dist/utils/context/index.js +176 -5
  53. package/dist/utils/context/interface.d.ts +1 -1
  54. package/dist/utils/context/interface.d.ts.map +1 -1
  55. package/dist/utils/crypto/index.d.ts +1 -0
  56. package/dist/utils/crypto/index.d.ts.map +1 -1
  57. package/dist/utils/crypto/index.js +6 -2
  58. package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
  59. package/dist/utils/initializer/exposeRoutes.js +11 -4
  60. package/dist/utils/initializer/registerPlugins.d.ts +3 -1
  61. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  62. package/dist/utils/initializer/registerPlugins.js +9 -6
  63. package/dist/utils/roles/helpers.js +11 -3
  64. package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
  65. package/dist/utils/roles/machines/commonValidators.js +10 -6
  66. package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
  67. package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
  68. package/dist/utils/roles/machines/read/B/validators.js +8 -0
  69. package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
  70. package/dist/utils/roles/machines/read/C/index.js +10 -7
  71. package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
  72. package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
  73. package/dist/utils/roles/machines/read/C/validators.js +29 -0
  74. package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
  75. package/dist/utils/roles/machines/read/D/index.js +13 -11
  76. package/dist/utils/rules.d.ts +1 -1
  77. package/dist/utils/rules.d.ts.map +1 -1
  78. package/dist/utils/rules.js +26 -17
  79. package/jest.config.ts +2 -12
  80. package/jest.setup.ts +28 -0
  81. package/package.json +1 -2
  82. package/src/auth/controller.ts +70 -4
  83. package/src/auth/plugins/jwt.test.ts +93 -0
  84. package/src/auth/plugins/jwt.ts +62 -3
  85. package/src/auth/providers/custom-function/controller.ts +22 -5
  86. package/src/auth/providers/local-userpass/controller.ts +168 -96
  87. package/src/auth/providers/local-userpass/dtos.ts +13 -2
  88. package/src/auth/utils.ts +51 -86
  89. package/src/constants.ts +17 -3
  90. package/src/fastify.d.ts +32 -15
  91. package/src/features/functions/controller.ts +51 -3
  92. package/src/features/functions/dtos.ts +3 -0
  93. package/src/features/functions/interface.ts +3 -0
  94. package/src/features/functions/utils.ts +29 -8
  95. package/src/features/rules/utils.ts +11 -2
  96. package/src/features/triggers/index.ts +43 -1
  97. package/src/features/triggers/utils.ts +146 -38
  98. package/src/index.ts +69 -20
  99. package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
  100. package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
  101. package/src/services/mongodb-atlas/index.ts +241 -90
  102. package/src/services/mongodb-atlas/model.ts +15 -2
  103. package/src/services/mongodb-atlas/utils.ts +158 -22
  104. package/src/shared/handleUserRegistration.ts +5 -4
  105. package/src/shared/models/handleUserRegistration.model.ts +8 -3
  106. package/src/types/fastify-raw-body.d.ts +22 -0
  107. package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
  108. package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
  109. package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
  110. package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
  111. package/src/utils/__tests__/registerPlugins.test.ts +16 -1
  112. package/src/utils/context/helpers.ts +3 -0
  113. package/src/utils/context/index.ts +238 -13
  114. package/src/utils/context/interface.ts +1 -1
  115. package/src/utils/crypto/index.ts +5 -1
  116. package/src/utils/initializer/exposeRoutes.ts +15 -8
  117. package/src/utils/initializer/registerPlugins.ts +15 -7
  118. package/src/utils/roles/helpers.ts +23 -5
  119. package/src/utils/roles/machines/commonValidators.ts +10 -5
  120. package/src/utils/roles/machines/read/B/validators.ts +8 -0
  121. package/src/utils/roles/machines/read/C/index.ts +11 -7
  122. package/src/utils/roles/machines/read/C/validators.ts +21 -0
  123. package/src/utils/roles/machines/read/D/index.ts +22 -12
  124. package/src/utils/rules.ts +31 -22
  125. package/tsconfig.spec.json +7 -0
@@ -0,0 +1,93 @@
1
+ jest.mock('node:diagnostics_channel', () => {
2
+ const createChannel = () => ({
3
+ publish: jest.fn(),
4
+ subscribe: jest.fn()
5
+ })
6
+ return {
7
+ channel: jest.fn(createChannel),
8
+ tracingChannel: () => ({
9
+ asyncStart: createChannel(),
10
+ asyncEnd: createChannel(),
11
+ error: createChannel()
12
+ })
13
+ }
14
+ })
15
+
16
+ import fastify, { FastifyInstance, FastifyReply } from 'fastify'
17
+ import jwtAuthPlugin from './jwt'
18
+ import { ObjectId } from 'bson'
19
+
20
+ const SECRET = 'test-secret'
21
+
22
+ const createAccessRequest = (payload: { typ: 'access'; sub: string; iat: number }) => {
23
+ const request: Record<string, unknown> = {}
24
+ request.jwtVerify = jest.fn(async () => {
25
+ request.user = payload
26
+ })
27
+ return request
28
+ }
29
+
30
+ describe('jwtAuthentication', () => {
31
+ let app: FastifyInstance
32
+
33
+ beforeEach(async () => {
34
+ app = fastify()
35
+ await app.register(jwtAuthPlugin, { secret: SECRET })
36
+ await app.ready()
37
+ })
38
+
39
+ afterEach(async () => {
40
+ await app.close()
41
+ })
42
+
43
+ const setupMongo = (userPayload: { _id: ObjectId; lastLogoutAt?: Date }) => {
44
+ const findOneMock = jest.fn().mockResolvedValue(userPayload)
45
+ const collectionMock = { findOne: findOneMock }
46
+ const dbMock = { collection: jest.fn().mockReturnValue(collectionMock) }
47
+ const mongoMock = { client: { db: jest.fn().mockReturnValue(dbMock) } }
48
+ ;(app as any).mongo = mongoMock
49
+ }
50
+
51
+ const createReply = () => {
52
+ return {
53
+ code: jest.fn().mockReturnThis(),
54
+ send: jest.fn()
55
+ } as unknown as FastifyReply
56
+ }
57
+
58
+ it('allows access tokens issued after the last logout', async () => {
59
+ const userId = new ObjectId()
60
+ const nowSeconds = Math.floor(Date.now() / 1000)
61
+ setupMongo({ _id: userId, lastLogoutAt: new Date((nowSeconds - 30) * 1000) })
62
+
63
+ const request = createAccessRequest({
64
+ typ: 'access',
65
+ sub: userId.toHexString(),
66
+ iat: nowSeconds
67
+ })
68
+ const reply = createReply()
69
+
70
+ await app.jwtAuthentication(request as any, reply)
71
+
72
+ expect(reply.code).not.toHaveBeenCalled()
73
+ expect(reply.send).not.toHaveBeenCalled()
74
+ })
75
+
76
+ it('rejects access tokens issued before the last logout', async () => {
77
+ const userId = new ObjectId()
78
+ const nowSeconds = Math.floor(Date.now() / 1000)
79
+ setupMongo({ _id: userId, lastLogoutAt: new Date((nowSeconds + 30) * 1000) })
80
+
81
+ const request = createAccessRequest({
82
+ typ: 'access',
83
+ sub: userId.toHexString(),
84
+ iat: nowSeconds
85
+ })
86
+ const reply = createReply()
87
+
88
+ await app.jwtAuthentication(request as any, reply)
89
+
90
+ expect(reply.code).toHaveBeenCalledWith(401)
91
+ expect(reply.send).toHaveBeenCalledWith({ message: 'Unauthorized' })
92
+ })
93
+ })
@@ -1,11 +1,18 @@
1
1
  import fastifyJwt from '@fastify/jwt'
2
2
  import fp from 'fastify-plugin'
3
3
  import { Document, ObjectId, WithId } from 'mongodb'
4
+ import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../constants'
4
5
 
5
6
  type Options = {
6
7
  secret: string
7
8
  }
8
9
 
10
+ type JwtAccessWithTimestamp = {
11
+ typ: 'access'
12
+ sub: string
13
+ iat?: number
14
+ }
15
+
9
16
  /**
10
17
  * This module is a Fastify plugin that sets up JWT-based authentication and token creation.
11
18
  * It registers JWT authentication, and provides methods to create access and refresh tokens.
@@ -25,8 +32,60 @@ export default fp(async function (fastify, opts: Options) {
25
32
  try {
26
33
  await request.jwtVerify()
27
34
  } catch (err) {
28
- // TODO: handle error
29
- reply.send(err)
35
+ fastify.log.warn({ err }, 'JWT authentication failed')
36
+ reply.code(401).send({ message: 'Unauthorized' })
37
+ return
38
+ }
39
+
40
+ if (request.user?.typ !== 'access') {
41
+ return
42
+ }
43
+
44
+ const db = fastify.mongo?.client?.db(DB_NAME)
45
+ if (!db) {
46
+ fastify.log.warn('Mongo client unavailable while checking logout state')
47
+ return
48
+ }
49
+
50
+ if (!request.user.sub) {
51
+ reply.code(401).send({ message: 'Unauthorized' })
52
+ return
53
+ }
54
+
55
+ let authUser
56
+ try {
57
+ authUser = await db
58
+ .collection<Document>(AUTH_CONFIG.authCollection)
59
+ .findOne({ _id: new ObjectId(request.user.sub) })
60
+ } catch (err) {
61
+ fastify.log.warn({ err }, 'Failed to lookup user during JWT authentication')
62
+ reply.code(401).send({ message: 'Unauthorized' })
63
+ return
64
+ }
65
+
66
+ if (!authUser) {
67
+ reply.code(401).send({ message: 'Unauthorized' })
68
+ return
69
+ }
70
+
71
+ const lastLogoutAt = authUser.lastLogoutAt ? new Date(authUser.lastLogoutAt) : null
72
+ const accessUser = request.user as JwtAccessWithTimestamp
73
+ const rawIssuedAt = accessUser.iat
74
+ const issuedAt =
75
+ typeof rawIssuedAt === 'number'
76
+ ? rawIssuedAt
77
+ : typeof rawIssuedAt === 'string'
78
+ ? Number(rawIssuedAt)
79
+ : undefined
80
+ if (
81
+ lastLogoutAt &&
82
+ !Number.isNaN(lastLogoutAt.getTime()) &&
83
+ typeof issuedAt === 'number' &&
84
+ !Number.isNaN(issuedAt) &&
85
+ lastLogoutAt.getTime() >= issuedAt * 1000
86
+ ) {
87
+ reply.code(401).send({ message: 'Unauthorized' })
88
+ return
30
89
  }
31
90
  })
32
91
 
@@ -66,7 +125,7 @@ export default fp(async function (fastify, opts: Options) {
66
125
  },
67
126
  {
68
127
  sub: user._id.toJSON(),
69
- expiresIn: '60d'
128
+ expiresIn: `${DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS}d`
70
129
  }
71
130
  )
72
131
  })
@@ -1,9 +1,10 @@
1
1
  import { FastifyInstance } from 'fastify'
2
- import { AUTH_CONFIG } from '../../../constants'
2
+ import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
3
3
  import handleUserRegistration from '../../../shared/handleUserRegistration'
4
4
  import { PROVIDER } from '../../../shared/models/handleUserRegistration.model'
5
5
  import { StateManager } from '../../../state'
6
6
  import { GenerateContext } from '../../../utils/context'
7
+ import { hashToken } from '../../../utils/crypto'
7
8
  import {
8
9
  AUTH_ENDPOINTS,
9
10
  generatePassword,
@@ -22,6 +23,9 @@ export async function customFunctionController(app: FastifyInstance) {
22
23
 
23
24
  const functionsList = StateManager.select('functions')
24
25
  const services = StateManager.select('services')
26
+ const db = app.mongo.client.db(DB_NAME)
27
+ const { refreshTokensCollection } = AUTH_CONFIG
28
+ const refreshTokenTtlMs = DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000
25
29
 
26
30
  /**
27
31
  * Endpoint for user login.
@@ -53,6 +57,7 @@ export async function customFunctionController(app: FastifyInstance) {
53
57
  id
54
58
  } = req
55
59
 
60
+ type CustomFunctionAuthResult = { id?: string }
56
61
  const res = await GenerateContext({
57
62
  args: [
58
63
  req.body
@@ -72,23 +77,35 @@ export async function customFunctionController(app: FastifyInstance) {
72
77
  ip,
73
78
  id
74
79
  }
75
- })
80
+ }) as CustomFunctionAuthResult
76
81
 
77
82
 
78
83
  if (res.id) {
79
84
  const user = await handleUserRegistration(app, { run_as_system: true, skipUserCheck: true, provider: PROVIDER.CUSTOM_FUNCTION })({ email: res.id, password: generatePassword() })
85
+ if (!user?.insertedId) {
86
+ throw new Error('Failed to register custom user')
87
+ }
80
88
 
81
89
  const currentUserData = {
82
90
  _id: user.insertedId,
83
91
  user_data: {
84
- _id: user.insertedId,
92
+ _id: user.insertedId
85
93
  }
86
94
  }
95
+ const refreshToken = this.createRefreshToken(currentUserData)
96
+ const refreshTokenHash = hashToken(refreshToken)
97
+ await db.collection(refreshTokensCollection).insertOne({
98
+ userId: user.insertedId,
99
+ tokenHash: refreshTokenHash,
100
+ createdAt: new Date(),
101
+ expiresAt: new Date(Date.now() + refreshTokenTtlMs),
102
+ revokedAt: null
103
+ })
87
104
  return {
88
105
  access_token: this.createAccessToken(currentUserData),
89
- refresh_token: this.createRefreshToken(currentUserData),
106
+ refresh_token: refreshToken,
90
107
  device_id: '',
91
- user_id: user.insertedId.toString(),
108
+ user_id: user.insertedId.toString()
92
109
  }
93
110
  }
94
111
 
@@ -1,42 +1,119 @@
1
- import sendGrid from '@sendgrid/mail'
2
1
  import { FastifyInstance } from 'fastify'
3
- import { AUTH_CONFIG, DB_NAME } from '../../../constants'
2
+ import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
4
3
  import { services } from '../../../services'
5
4
  import handleUserRegistration from '../../../shared/handleUserRegistration'
6
5
  import { PROVIDER } from '../../../shared/models/handleUserRegistration.model'
7
6
  import { StateManager } from '../../../state'
8
7
  import { GenerateContext } from '../../../utils/context'
9
- import { comparePassword, generateToken, hashPassword } from '../../../utils/crypto'
8
+ import { comparePassword, generateToken, hashPassword, hashToken } from '../../../utils/crypto'
10
9
  import {
11
10
  AUTH_ENDPOINTS,
12
11
  AUTH_ERRORS,
13
12
  CONFIRM_RESET_SCHEMA,
14
- getMailConfig,
15
13
  LOGIN_SCHEMA,
16
14
  REGISTRATION_SCHEMA,
17
- RESET_SCHEMA
15
+ RESET_CALL_SCHEMA,
16
+ RESET_SEND_SCHEMA
18
17
  } from '../../utils'
19
18
  import {
20
19
  ConfirmResetPasswordDto,
21
20
  LoginDto,
22
21
  RegistrationDto,
23
- ResetPasswordDto
22
+ ResetPasswordCallDto,
23
+ ResetPasswordSendDto
24
24
  } from './dtos'
25
+
26
+ const rateLimitStore = new Map<string, number[]>()
27
+
28
+ const isRateLimited = (key: string, maxAttempts: number, windowMs: number) => {
29
+ const now = Date.now()
30
+ const existing = rateLimitStore.get(key) ?? []
31
+ const recent = existing.filter((timestamp) => now - timestamp < windowMs)
32
+ recent.push(now)
33
+ rateLimitStore.set(key, recent)
34
+ return recent.length > maxAttempts
35
+ }
25
36
  /**
26
37
  * Controller for handling local user registration and login.
27
38
  * @testable
28
39
  * @param {FastifyInstance} app - The Fastify instance.
29
40
  */
30
41
  export async function localUserPassController(app: FastifyInstance) {
31
- const functionsList = StateManager.select('functions')
32
-
33
- const {
34
- authCollection,
35
- userCollection,
36
- user_id_field,
37
- on_user_creation_function_name
38
- } = AUTH_CONFIG
42
+ const { authCollection, userCollection, user_id_field } = AUTH_CONFIG
43
+ const { resetPasswordCollection } = AUTH_CONFIG
44
+ const { refreshTokensCollection } = AUTH_CONFIG
39
45
  const db = app.mongo.client.db(DB_NAME)
46
+ const resetPasswordTtlSeconds = DEFAULT_CONFIG.RESET_PASSWORD_TTL_SECONDS
47
+ const rateLimitWindowMs = DEFAULT_CONFIG.AUTH_RATE_LIMIT_WINDOW_MS
48
+ const loginMaxAttempts = DEFAULT_CONFIG.AUTH_LOGIN_MAX_ATTEMPTS
49
+ const resetMaxAttempts = DEFAULT_CONFIG.AUTH_RESET_MAX_ATTEMPTS
50
+ const refreshTokenTtlMs = DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000
51
+
52
+ try {
53
+ await db.collection(resetPasswordCollection).createIndex(
54
+ { createdAt: 1 },
55
+ { expireAfterSeconds: resetPasswordTtlSeconds }
56
+ )
57
+ } catch (error) {
58
+ console.error('Failed to ensure reset password TTL index', error)
59
+ }
60
+
61
+ try {
62
+ await db.collection(refreshTokensCollection).createIndex(
63
+ { expiresAt: 1 },
64
+ { expireAfterSeconds: 0 }
65
+ )
66
+ } catch (error) {
67
+ console.error('Failed to ensure refresh token TTL index', error)
68
+ }
69
+ const handleResetPasswordRequest = async (
70
+ email: string,
71
+ password?: string,
72
+ extraArguments?: unknown[]
73
+ ) => {
74
+ const { resetPasswordConfig } = AUTH_CONFIG
75
+ const authUser = await db.collection(authCollection!).findOne({
76
+ email
77
+ })
78
+
79
+ if (!authUser) {
80
+ return
81
+ }
82
+
83
+ const token = generateToken()
84
+ const tokenId = generateToken()
85
+
86
+ await db
87
+ ?.collection(resetPasswordCollection)
88
+ .updateOne(
89
+ { email },
90
+ { $set: { token, tokenId, email, createdAt: new Date() } },
91
+ { upsert: true }
92
+ )
93
+
94
+ if (!resetPasswordConfig.runResetFunction && !resetPasswordConfig.resetFunctionName) {
95
+ throw new Error(AUTH_ERRORS.MISSING_RESET_FUNCTION)
96
+ }
97
+
98
+ if (resetPasswordConfig.runResetFunction && resetPasswordConfig.resetFunctionName) {
99
+ const functionsList = StateManager.select('functions')
100
+ const services = StateManager.select('services')
101
+ const currentFunction = functionsList[resetPasswordConfig.resetFunctionName]
102
+ const baseArgs = { token, tokenId, email, password, username: email }
103
+ const args = Array.isArray(extraArguments) ? [baseArgs, ...extraArguments] : [baseArgs]
104
+ await GenerateContext({
105
+ args,
106
+ app,
107
+ rules: {},
108
+ user: {},
109
+ currentFunction,
110
+ functionsList,
111
+ services
112
+ })
113
+ return
114
+ }
115
+
116
+ }
40
117
 
41
118
  /**
42
119
  * Endpoint for user registration.
@@ -55,8 +132,13 @@ export async function localUserPassController(app: FastifyInstance) {
55
132
 
56
133
  const result = await handleUserRegistration(app, { run_as_system: true, provider: PROVIDER.LOCAL_USERPASS })({ email: req.body.email.toLowerCase(), password: req.body.password })
57
134
 
135
+ if (!result?.insertedId) {
136
+ res?.status(500)
137
+ throw new Error('Failed to register user')
138
+ }
139
+
58
140
  res?.status(201)
59
- return { userId: result?.insertedId.toString() }
141
+ return { userId: result.insertedId.toString() }
60
142
  }
61
143
  )
62
144
 
@@ -72,7 +154,12 @@ export async function localUserPassController(app: FastifyInstance) {
72
154
  {
73
155
  schema: LOGIN_SCHEMA
74
156
  },
75
- async function (req) {
157
+ async function (req, res) {
158
+ const key = `login:${req.ip}`
159
+ if (isRateLimited(key, loginMaxAttempts, rateLimitWindowMs)) {
160
+ res.status(429).send({ message: 'Too many requests' })
161
+ return
162
+ }
76
163
  const authUser = await db.collection(authCollection!).findOne({
77
164
  email: req.body.username
78
165
  })
@@ -98,7 +185,12 @@ export async function localUserPassController(app: FastifyInstance) {
98
185
  : {}
99
186
  delete authUser?.password
100
187
 
101
- const userWithCustomData = { ...authUser, user_data: user, id: authUser._id.toString() }
188
+ const userWithCustomData = {
189
+ ...authUser,
190
+ user_data: { ...(user || {}), _id: authUser._id },
191
+ data: { email: authUser.email },
192
+ id: authUser._id.toString()
193
+ }
102
194
 
103
195
  if (authUser && authUser.status === 'pending') {
104
196
  try {
@@ -115,37 +207,19 @@ export async function localUserPassController(app: FastifyInstance) {
115
207
  }
116
208
  }
117
209
 
118
- if (
119
- authUser &&
120
- authUser.status === 'pending' &&
121
- on_user_creation_function_name &&
122
- functionsList[on_user_creation_function_name]
123
- ) {
124
- try {
125
- await GenerateContext({
126
- args: [
127
- {
128
- operationType: 'CREATE',
129
- providers: 'local-userpass',
130
- user: userWithCustomData,
131
- time: new Date().getTime()
132
- }
133
- ],
134
- app,
135
- rules: {},
136
- user: userWithCustomData,
137
- currentFunction: functionsList[on_user_creation_function_name],
138
- functionsList,
139
- services
140
- })
141
- } catch (error) {
142
- console.log('localUserPassController - /login - GenerateContext - CATCH:', error)
143
- }
144
- }
210
+ const refreshToken = this.createRefreshToken(userWithCustomData)
211
+ const refreshTokenHash = hashToken(refreshToken)
212
+ await db.collection(refreshTokensCollection).insertOne({
213
+ userId: authUser._id,
214
+ tokenHash: refreshTokenHash,
215
+ createdAt: new Date(),
216
+ expiresAt: new Date(Date.now() + refreshTokenTtlMs),
217
+ revokedAt: null
218
+ })
145
219
 
146
220
  return {
147
221
  access_token: this.createAccessToken(userWithCustomData),
148
- refresh_token: this.createRefreshToken(userWithCustomData),
222
+ refresh_token: refreshToken,
149
223
  device_id: '',
150
224
  user_id: authUser._id.toString()
151
225
  }
@@ -155,65 +229,49 @@ export async function localUserPassController(app: FastifyInstance) {
155
229
  /**
156
230
  * Endpoint for reset password.
157
231
  *
158
- * @route {POST} /reset/call
232
+ * @route {POST} /reset/send
159
233
  * @param {ResetPasswordDto} req - The request object with th reset request.
160
234
  * @returns {Promise<void>}
161
235
  */
162
- app.post<ResetPasswordDto>(
236
+ app.post<ResetPasswordSendDto>(
163
237
  AUTH_ENDPOINTS.RESET,
164
238
  {
165
- schema: RESET_SCHEMA
239
+ schema: RESET_SEND_SCHEMA
166
240
  },
167
- async function (req) {
168
- const { resetPasswordCollection, resetPasswordConfig } = AUTH_CONFIG
169
- const email = req.body.email
170
- const authUser = await db.collection(authCollection!).findOne({
171
- email
172
- })
173
-
174
- if (!authUser) {
175
- throw new Error(AUTH_ERRORS.INVALID_CREDENTIALS)
241
+ async function (req, res) {
242
+ const key = `reset:${req.ip}`
243
+ if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
244
+ res.status(429)
245
+ return { message: 'Too many requests' }
176
246
  }
177
-
178
- const token = generateToken()
179
- const tokenId = generateToken()
180
-
181
- await db
182
- ?.collection(resetPasswordCollection)
183
- .updateOne(
184
- { email },
185
- { $set: { token, tokenId, email, createdAt: new Date() } },
186
- { upsert: true }
187
- )
188
-
189
- if (resetPasswordConfig.runResetFunction && resetPasswordConfig.resetFunctionName) {
190
- const functionsList = StateManager.select('functions')
191
- const services = StateManager.select('services')
192
- const currentFunction = functionsList[resetPasswordConfig.resetFunctionName]
193
- await GenerateContext({
194
- args: [{ token, tokenId, email }],
195
- app,
196
- rules: {},
197
- user: {},
198
- currentFunction,
199
- functionsList,
200
- services
201
- })
202
- return
247
+ await handleResetPasswordRequest(req.body.email)
248
+ res.status(202)
249
+ return {
250
+ status: 'ok'
203
251
  }
252
+ }
253
+ )
204
254
 
205
- const { from, subject, mailToken, body } = getMailConfig(
206
- resetPasswordConfig,
207
- token,
208
- tokenId
255
+ app.post<ResetPasswordCallDto>(
256
+ AUTH_ENDPOINTS.RESET_CALL,
257
+ {
258
+ schema: RESET_CALL_SCHEMA
259
+ },
260
+ async function (req, res) {
261
+ const key = `reset:${req.ip}`
262
+ if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
263
+ res.status(429)
264
+ return { message: 'Too many requests' }
265
+ }
266
+ await handleResetPasswordRequest(
267
+ req.body.email,
268
+ req.body.password,
269
+ req.body.arguments
209
270
  )
210
- sendGrid.setApiKey(mailToken)
211
- await sendGrid.send({
212
- to: email,
213
- from,
214
- subject,
215
- html: body
216
- })
271
+ res.status(202)
272
+ return {
273
+ status: 'ok'
274
+ }
217
275
  }
218
276
  )
219
277
 
@@ -229,8 +287,12 @@ export async function localUserPassController(app: FastifyInstance) {
229
287
  {
230
288
  schema: CONFIRM_RESET_SCHEMA
231
289
  },
232
- async function (req) {
233
- const { resetPasswordCollection } = AUTH_CONFIG
290
+ async function (req, res) {
291
+ const key = `reset-confirm:${req.ip}`
292
+ if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
293
+ res.status(429)
294
+ return { message: 'Too many requests' }
295
+ }
234
296
  const { token, tokenId, password } = req.body
235
297
 
236
298
  const resetRequest = await db
@@ -240,6 +302,16 @@ export async function localUserPassController(app: FastifyInstance) {
240
302
  if (!resetRequest) {
241
303
  throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
242
304
  }
305
+
306
+ const createdAt = resetRequest.createdAt ? new Date(resetRequest.createdAt) : null
307
+ const isExpired = !createdAt ||
308
+ Number.isNaN(createdAt.getTime()) ||
309
+ Date.now() - createdAt.getTime() > resetPasswordTtlSeconds * 1000
310
+
311
+ if (isExpired) {
312
+ await db?.collection(resetPasswordCollection).deleteOne({ _id: resetRequest._id })
313
+ throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
314
+ }
243
315
  const hashedPassword = await hashPassword(password)
244
316
  await db.collection(authCollection!).updateOne(
245
317
  { email: resetRequest.email },
@@ -15,19 +15,30 @@ export type LoginSuccessDto = {
15
15
  user_id: string
16
16
  }
17
17
 
18
+ export type ErrorResponseDto = {
19
+ message: string
20
+ }
21
+
18
22
  export interface RegistrationDto {
19
23
  Body: RegisterUserDto
20
24
  }
21
25
 
22
26
  export interface LoginDto {
23
27
  Body: LoginUserDto
24
- Reply: LoginSuccessDto
28
+ Reply: LoginSuccessDto | ErrorResponseDto
29
+ }
30
+
31
+ export interface ResetPasswordSendDto {
32
+ Body: {
33
+ email: string
34
+ }
25
35
  }
26
36
 
27
- export interface ResetPasswordDto {
37
+ export interface ResetPasswordCallDto {
28
38
  Body: {
29
39
  email: string
30
40
  password: string
41
+ arguments?: unknown[]
31
42
  }
32
43
  }
33
44