@flowerforce/flowerbase 1.2.1-beta.2 → 1.2.1-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -6
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +55 -4
- package/dist/auth/plugins/jwt.d.ts.map +1 -1
- package/dist/auth/plugins/jwt.js +52 -6
- package/dist/auth/providers/anon-user/controller.d.ts +8 -0
- package/dist/auth/providers/anon-user/controller.d.ts.map +1 -0
- package/dist/auth/providers/anon-user/controller.js +90 -0
- package/dist/auth/providers/anon-user/dtos.d.ts +10 -0
- package/dist/auth/providers/anon-user/dtos.d.ts.map +1 -0
- package/dist/auth/providers/anon-user/dtos.js +2 -0
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +35 -25
- package/dist/auth/providers/custom-function/dtos.d.ts +4 -1
- package/dist/auth/providers/custom-function/dtos.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +159 -73
- package/dist/auth/providers/local-userpass/dtos.d.ts +17 -2
- package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
- package/dist/auth/utils.d.ts +76 -14
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +55 -61
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -4
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +31 -12
- package/dist/features/functions/dtos.d.ts +3 -0
- package/dist/features/functions/dtos.d.ts.map +1 -1
- package/dist/features/functions/interface.d.ts +3 -0
- package/dist/features/functions/interface.d.ts.map +1 -1
- package/dist/features/functions/utils.d.ts +3 -2
- package/dist/features/functions/utils.d.ts.map +1 -1
- package/dist/features/functions/utils.js +19 -7
- package/dist/features/triggers/index.d.ts.map +1 -1
- package/dist/features/triggers/index.js +49 -7
- package/dist/features/triggers/interface.d.ts +1 -0
- package/dist/features/triggers/interface.d.ts.map +1 -1
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +67 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -13
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +72 -2
- package/dist/services/mongodb-atlas/model.d.ts +3 -2
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.js +3 -1
- package/dist/shared/handleUserRegistration.d.ts.map +1 -1
- package/dist/shared/handleUserRegistration.js +66 -1
- package/dist/shared/models/handleUserRegistration.model.d.ts +2 -1
- package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
- package/dist/shared/models/handleUserRegistration.model.js +1 -0
- package/dist/utils/context/helpers.d.ts +6 -6
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/context/index.d.ts +1 -1
- package/dist/utils/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +176 -9
- package/dist/utils/context/interface.d.ts +1 -1
- package/dist/utils/context/interface.d.ts.map +1 -1
- package/dist/utils/crypto/index.d.ts +1 -0
- package/dist/utils/crypto/index.d.ts.map +1 -1
- package/dist/utils/crypto/index.js +6 -2
- package/dist/utils/initializer/exposeRoutes.js +1 -1
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +12 -4
- package/dist/utils/roles/helpers.js +2 -1
- package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
- package/dist/utils/rules-matcher/utils.js +3 -0
- package/package.json +1 -2
- package/src/auth/controller.ts +71 -5
- package/src/auth/plugins/jwt.test.ts +93 -0
- package/src/auth/plugins/jwt.ts +67 -8
- package/src/auth/providers/anon-user/controller.ts +91 -0
- package/src/auth/providers/anon-user/dtos.ts +10 -0
- package/src/auth/providers/custom-function/controller.ts +40 -31
- package/src/auth/providers/custom-function/dtos.ts +5 -1
- package/src/auth/providers/local-userpass/controller.ts +211 -101
- package/src/auth/providers/local-userpass/dtos.ts +20 -2
- package/src/auth/utils.ts +66 -83
- package/src/constants.ts +14 -2
- package/src/features/functions/controller.ts +42 -12
- package/src/features/functions/dtos.ts +3 -0
- package/src/features/functions/interface.ts +3 -0
- package/src/features/functions/utils.ts +29 -8
- package/src/features/triggers/index.ts +44 -1
- package/src/features/triggers/interface.ts +1 -0
- package/src/features/triggers/utils.ts +89 -37
- package/src/index.ts +49 -13
- package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
- package/src/services/mongodb-atlas/index.ts +665 -567
- package/src/services/mongodb-atlas/model.ts +16 -3
- package/src/services/mongodb-atlas/utils.ts +3 -0
- package/src/shared/handleUserRegistration.ts +83 -2
- package/src/shared/models/handleUserRegistration.model.ts +2 -1
- package/src/utils/__tests__/registerPlugins.test.ts +5 -1
- package/src/utils/context/index.ts +238 -18
- package/src/utils/context/interface.ts +1 -1
- package/src/utils/crypto/index.ts +5 -1
- package/src/utils/initializer/exposeRoutes.ts +1 -1
- package/src/utils/initializer/registerPlugins.ts +8 -0
- package/src/utils/roles/helpers.ts +3 -2
- package/src/utils/rules-matcher/utils.ts +3 -0
|
@@ -1,42 +1,122 @@
|
|
|
1
|
-
import sendGrid from '@sendgrid/mail'
|
|
2
1
|
import { FastifyInstance } from 'fastify'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { ObjectId } from 'mongodb'
|
|
3
|
+
import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
|
|
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
|
-
|
|
13
|
+
CONFIRM_USER_SCHEMA,
|
|
15
14
|
LOGIN_SCHEMA,
|
|
16
15
|
REGISTRATION_SCHEMA,
|
|
17
|
-
|
|
16
|
+
RESET_CALL_SCHEMA,
|
|
17
|
+
RESET_SEND_SCHEMA
|
|
18
18
|
} from '../../utils'
|
|
19
19
|
import {
|
|
20
20
|
ConfirmResetPasswordDto,
|
|
21
|
+
ConfirmUserDto,
|
|
21
22
|
LoginDto,
|
|
22
23
|
RegistrationDto,
|
|
23
|
-
|
|
24
|
+
ResetPasswordCallDto,
|
|
25
|
+
ResetPasswordSendDto
|
|
24
26
|
} from './dtos'
|
|
27
|
+
|
|
28
|
+
const rateLimitStore = new Map<string, number[]>()
|
|
29
|
+
|
|
30
|
+
const isRateLimited = (key: string, maxAttempts: number, windowMs: number) => {
|
|
31
|
+
const now = Date.now()
|
|
32
|
+
const existing = rateLimitStore.get(key) ?? []
|
|
33
|
+
const recent = existing.filter((timestamp) => now - timestamp < windowMs)
|
|
34
|
+
recent.push(now)
|
|
35
|
+
rateLimitStore.set(key, recent)
|
|
36
|
+
return recent.length > maxAttempts
|
|
37
|
+
}
|
|
25
38
|
/**
|
|
26
39
|
* Controller for handling local user registration and login.
|
|
27
40
|
* @testable
|
|
28
41
|
* @param {FastifyInstance} app - The Fastify instance.
|
|
29
42
|
*/
|
|
30
43
|
export async function localUserPassController(app: FastifyInstance) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const {
|
|
34
|
-
authCollection,
|
|
35
|
-
userCollection,
|
|
36
|
-
user_id_field,
|
|
37
|
-
on_user_creation_function_name
|
|
38
|
-
} = AUTH_CONFIG
|
|
44
|
+
const { authCollection, userCollection, user_id_field } = AUTH_CONFIG
|
|
45
|
+
const { resetPasswordCollection } = AUTH_CONFIG
|
|
46
|
+
const { refreshTokensCollection } = AUTH_CONFIG
|
|
39
47
|
const db = app.mongo.client.db(DB_NAME)
|
|
48
|
+
const resetPasswordTtlSeconds = DEFAULT_CONFIG.RESET_PASSWORD_TTL_SECONDS
|
|
49
|
+
const rateLimitWindowMs = DEFAULT_CONFIG.AUTH_RATE_LIMIT_WINDOW_MS
|
|
50
|
+
const loginMaxAttempts = DEFAULT_CONFIG.AUTH_LOGIN_MAX_ATTEMPTS
|
|
51
|
+
const registerMaxAttempts = DEFAULT_CONFIG.AUTH_REGISTER_MAX_ATTEMPTS
|
|
52
|
+
const resetMaxAttempts = DEFAULT_CONFIG.AUTH_RESET_MAX_ATTEMPTS
|
|
53
|
+
const refreshTokenTtlMs = DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await db.collection(resetPasswordCollection).createIndex(
|
|
57
|
+
{ createdAt: 1 },
|
|
58
|
+
{ expireAfterSeconds: resetPasswordTtlSeconds }
|
|
59
|
+
)
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Failed to ensure reset password TTL index', error)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await db.collection(refreshTokensCollection).createIndex(
|
|
66
|
+
{ expiresAt: 1 },
|
|
67
|
+
{ expireAfterSeconds: 0 }
|
|
68
|
+
)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Failed to ensure refresh token TTL index', error)
|
|
71
|
+
}
|
|
72
|
+
const handleResetPasswordRequest = async (
|
|
73
|
+
email: string,
|
|
74
|
+
password?: string,
|
|
75
|
+
extraArguments?: unknown[]
|
|
76
|
+
) => {
|
|
77
|
+
const { resetPasswordConfig } = AUTH_CONFIG
|
|
78
|
+
const authUser = await db.collection(authCollection!).findOne({
|
|
79
|
+
email
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if (!authUser) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const token = generateToken()
|
|
87
|
+
const tokenId = generateToken()
|
|
88
|
+
|
|
89
|
+
await db
|
|
90
|
+
?.collection(resetPasswordCollection)
|
|
91
|
+
.updateOne(
|
|
92
|
+
{ email },
|
|
93
|
+
{ $set: { token, tokenId, email, createdAt: new Date() } },
|
|
94
|
+
{ upsert: true }
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if (!resetPasswordConfig.runResetFunction && !resetPasswordConfig.resetFunctionName) {
|
|
98
|
+
throw new Error(AUTH_ERRORS.MISSING_RESET_FUNCTION)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (resetPasswordConfig.runResetFunction && resetPasswordConfig.resetFunctionName) {
|
|
102
|
+
const functionsList = StateManager.select('functions')
|
|
103
|
+
const services = StateManager.select('services')
|
|
104
|
+
const currentFunction = functionsList[resetPasswordConfig.resetFunctionName]
|
|
105
|
+
const baseArgs = { token, tokenId, email, password, username: email }
|
|
106
|
+
const args = Array.isArray(extraArguments) ? [baseArgs, ...extraArguments] : [baseArgs]
|
|
107
|
+
await GenerateContext({
|
|
108
|
+
args,
|
|
109
|
+
app,
|
|
110
|
+
rules: {},
|
|
111
|
+
user: {},
|
|
112
|
+
currentFunction,
|
|
113
|
+
functionsList,
|
|
114
|
+
services
|
|
115
|
+
})
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
}
|
|
40
120
|
|
|
41
121
|
/**
|
|
42
122
|
* Endpoint for user registration.
|
|
@@ -52,6 +132,11 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
52
132
|
schema: REGISTRATION_SCHEMA
|
|
53
133
|
},
|
|
54
134
|
async (req, res) => {
|
|
135
|
+
const key = `register:${req.ip}`
|
|
136
|
+
if (isRateLimited(key, registerMaxAttempts, rateLimitWindowMs)) {
|
|
137
|
+
res.status(429).send({ message: 'Too many requests' })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
55
140
|
|
|
56
141
|
const result = await handleUserRegistration(app, { run_as_system: true, provider: PROVIDER.LOCAL_USERPASS })({ email: req.body.email.toLowerCase(), password: req.body.password })
|
|
57
142
|
|
|
@@ -65,6 +150,50 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
65
150
|
}
|
|
66
151
|
)
|
|
67
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Endpoint for confirming a user registration.
|
|
155
|
+
*
|
|
156
|
+
* @route {POST} /confirm
|
|
157
|
+
* @param {ConfirmUserDto} req - The request object with confirmation data.
|
|
158
|
+
* @returns {Promise<Object>} A promise resolving with confirmation status.
|
|
159
|
+
*/
|
|
160
|
+
app.post<ConfirmUserDto>(
|
|
161
|
+
AUTH_ENDPOINTS.CONFIRM,
|
|
162
|
+
{
|
|
163
|
+
schema: CONFIRM_USER_SCHEMA
|
|
164
|
+
},
|
|
165
|
+
async (req, res) => {
|
|
166
|
+
const key = `confirm:${req.ip}`
|
|
167
|
+
if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
|
|
168
|
+
res.status(429).send({ message: 'Too many requests' })
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const existing = await db.collection(authCollection!).findOne({
|
|
173
|
+
confirmationToken: req.body.token,
|
|
174
|
+
confirmationTokenId: req.body.tokenId
|
|
175
|
+
}) as { _id: ObjectId; status?: string } | null
|
|
176
|
+
|
|
177
|
+
if (!existing) {
|
|
178
|
+
res.status(500)
|
|
179
|
+
throw new Error(AUTH_ERRORS.INVALID_TOKEN)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (existing.status !== 'confirmed') {
|
|
183
|
+
await db.collection(authCollection!).updateOne(
|
|
184
|
+
{ _id: existing._id },
|
|
185
|
+
{
|
|
186
|
+
$set: { status: 'confirmed' },
|
|
187
|
+
$unset: { confirmationToken: '', confirmationTokenId: '' }
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
res.status(200)
|
|
193
|
+
return { status: 'confirmed' }
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
68
197
|
/**
|
|
69
198
|
* Endpoint for user login.
|
|
70
199
|
*
|
|
@@ -77,7 +206,12 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
77
206
|
{
|
|
78
207
|
schema: LOGIN_SCHEMA
|
|
79
208
|
},
|
|
80
|
-
async function (req) {
|
|
209
|
+
async function (req, res) {
|
|
210
|
+
const key = `login:${req.ip}`
|
|
211
|
+
if (isRateLimited(key, loginMaxAttempts, rateLimitWindowMs)) {
|
|
212
|
+
res.status(429).send({ message: 'Too many requests' })
|
|
213
|
+
return
|
|
214
|
+
}
|
|
81
215
|
const authUser = await db.collection(authCollection!).findOne({
|
|
82
216
|
email: req.body.username
|
|
83
217
|
})
|
|
@@ -110,45 +244,23 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
110
244
|
id: authUser._id.toString()
|
|
111
245
|
}
|
|
112
246
|
|
|
113
|
-
if (authUser && authUser.status
|
|
114
|
-
|
|
115
|
-
await db?.collection(authCollection!).updateOne(
|
|
116
|
-
{ _id: authUser._id },
|
|
117
|
-
{
|
|
118
|
-
$set: {
|
|
119
|
-
status: 'confirmed'
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
)
|
|
123
|
-
} catch (error) {
|
|
124
|
-
console.log('>>> 🚀 ~ localUserPassController ~ error:', error)
|
|
125
|
-
}
|
|
247
|
+
if (authUser && authUser.status !== 'confirmed') {
|
|
248
|
+
throw new Error(AUTH_ERRORS.USER_NOT_CONFIRMED)
|
|
126
249
|
}
|
|
127
250
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
app,
|
|
138
|
-
rules: {},
|
|
139
|
-
user: userWithCustomData,
|
|
140
|
-
currentFunction: functionsList[on_user_creation_function_name],
|
|
141
|
-
functionsList,
|
|
142
|
-
services
|
|
143
|
-
})
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.log('localUserPassController - /login - GenerateContext - CATCH:', error)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
251
|
+
const refreshToken = this.createRefreshToken(userWithCustomData)
|
|
252
|
+
const refreshTokenHash = hashToken(refreshToken)
|
|
253
|
+
await db.collection(refreshTokensCollection).insertOne({
|
|
254
|
+
userId: authUser._id,
|
|
255
|
+
tokenHash: refreshTokenHash,
|
|
256
|
+
createdAt: new Date(),
|
|
257
|
+
expiresAt: new Date(Date.now() + refreshTokenTtlMs),
|
|
258
|
+
revokedAt: null
|
|
259
|
+
})
|
|
148
260
|
|
|
149
261
|
return {
|
|
150
262
|
access_token: this.createAccessToken(userWithCustomData),
|
|
151
|
-
refresh_token:
|
|
263
|
+
refresh_token: refreshToken,
|
|
152
264
|
device_id: '',
|
|
153
265
|
user_id: authUser._id.toString()
|
|
154
266
|
}
|
|
@@ -158,65 +270,49 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
158
270
|
/**
|
|
159
271
|
* Endpoint for reset password.
|
|
160
272
|
*
|
|
161
|
-
* @route {POST} /reset/
|
|
273
|
+
* @route {POST} /reset/send
|
|
162
274
|
* @param {ResetPasswordDto} req - The request object with th reset request.
|
|
163
275
|
* @returns {Promise<void>}
|
|
164
276
|
*/
|
|
165
|
-
app.post<
|
|
277
|
+
app.post<ResetPasswordSendDto>(
|
|
166
278
|
AUTH_ENDPOINTS.RESET,
|
|
167
279
|
{
|
|
168
|
-
schema:
|
|
280
|
+
schema: RESET_SEND_SCHEMA
|
|
169
281
|
},
|
|
170
|
-
async function (req) {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
if (!authUser) {
|
|
178
|
-
throw new Error(AUTH_ERRORS.INVALID_CREDENTIALS)
|
|
282
|
+
async function (req, res) {
|
|
283
|
+
const key = `reset:${req.ip}`
|
|
284
|
+
if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
|
|
285
|
+
res.status(429)
|
|
286
|
+
return { message: 'Too many requests' }
|
|
179
287
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
await db
|
|
185
|
-
?.collection(resetPasswordCollection)
|
|
186
|
-
.updateOne(
|
|
187
|
-
{ email },
|
|
188
|
-
{ $set: { token, tokenId, email, createdAt: new Date() } },
|
|
189
|
-
{ upsert: true }
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
if (resetPasswordConfig.runResetFunction && resetPasswordConfig.resetFunctionName) {
|
|
193
|
-
const functionsList = StateManager.select('functions')
|
|
194
|
-
const services = StateManager.select('services')
|
|
195
|
-
const currentFunction = functionsList[resetPasswordConfig.resetFunctionName]
|
|
196
|
-
await GenerateContext({
|
|
197
|
-
args: [{ token, tokenId, email }],
|
|
198
|
-
app,
|
|
199
|
-
rules: {},
|
|
200
|
-
user: {},
|
|
201
|
-
currentFunction,
|
|
202
|
-
functionsList,
|
|
203
|
-
services
|
|
204
|
-
})
|
|
205
|
-
return
|
|
288
|
+
await handleResetPasswordRequest(req.body.email)
|
|
289
|
+
res.status(202)
|
|
290
|
+
return {
|
|
291
|
+
status: 'ok'
|
|
206
292
|
}
|
|
293
|
+
}
|
|
294
|
+
)
|
|
207
295
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
296
|
+
app.post<ResetPasswordCallDto>(
|
|
297
|
+
AUTH_ENDPOINTS.RESET_CALL,
|
|
298
|
+
{
|
|
299
|
+
schema: RESET_CALL_SCHEMA
|
|
300
|
+
},
|
|
301
|
+
async function (req, res) {
|
|
302
|
+
const key = `reset:${req.ip}`
|
|
303
|
+
if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
|
|
304
|
+
res.status(429)
|
|
305
|
+
return { message: 'Too many requests' }
|
|
306
|
+
}
|
|
307
|
+
await handleResetPasswordRequest(
|
|
308
|
+
req.body.email,
|
|
309
|
+
req.body.password,
|
|
310
|
+
req.body.arguments
|
|
212
311
|
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
subject,
|
|
218
|
-
html: body
|
|
219
|
-
})
|
|
312
|
+
res.status(202)
|
|
313
|
+
return {
|
|
314
|
+
status: 'ok'
|
|
315
|
+
}
|
|
220
316
|
}
|
|
221
317
|
)
|
|
222
318
|
|
|
@@ -232,8 +328,12 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
232
328
|
{
|
|
233
329
|
schema: CONFIRM_RESET_SCHEMA
|
|
234
330
|
},
|
|
235
|
-
async function (req) {
|
|
236
|
-
const
|
|
331
|
+
async function (req, res) {
|
|
332
|
+
const key = `reset-confirm:${req.ip}`
|
|
333
|
+
if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
|
|
334
|
+
res.status(429)
|
|
335
|
+
return { message: 'Too many requests' }
|
|
336
|
+
}
|
|
237
337
|
const { token, tokenId, password } = req.body
|
|
238
338
|
|
|
239
339
|
const resetRequest = await db
|
|
@@ -243,6 +343,16 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
243
343
|
if (!resetRequest) {
|
|
244
344
|
throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
|
|
245
345
|
}
|
|
346
|
+
|
|
347
|
+
const createdAt = resetRequest.createdAt ? new Date(resetRequest.createdAt) : null
|
|
348
|
+
const isExpired = !createdAt ||
|
|
349
|
+
Number.isNaN(createdAt.getTime()) ||
|
|
350
|
+
Date.now() - createdAt.getTime() > resetPasswordTtlSeconds * 1000
|
|
351
|
+
|
|
352
|
+
if (isExpired) {
|
|
353
|
+
await db?.collection(resetPasswordCollection).deleteOne({ _id: resetRequest._id })
|
|
354
|
+
throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
|
|
355
|
+
}
|
|
246
356
|
const hashedPassword = await hashPassword(password)
|
|
247
357
|
await db.collection(authCollection!).updateOne(
|
|
248
358
|
{ 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
|
|
37
|
+
export interface ResetPasswordCallDto {
|
|
28
38
|
Body: {
|
|
29
39
|
email: string
|
|
30
40
|
password: string
|
|
41
|
+
arguments?: unknown[]
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
|
|
@@ -38,3 +49,10 @@ export interface ConfirmResetPasswordDto {
|
|
|
38
49
|
password: string
|
|
39
50
|
}
|
|
40
51
|
}
|
|
52
|
+
|
|
53
|
+
export interface ConfirmUserDto {
|
|
54
|
+
Body: {
|
|
55
|
+
token: string
|
|
56
|
+
tokenId: string
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/auth/utils.ts
CHANGED
|
@@ -8,19 +8,45 @@ export const LOGIN_SCHEMA = {
|
|
|
8
8
|
body: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
11
|
-
username: {
|
|
12
|
-
|
|
11
|
+
username: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
|
|
14
|
+
minLength: 3,
|
|
15
|
+
maxLength: 254
|
|
16
|
+
},
|
|
17
|
+
password: { type: 'string', minLength: 8, maxLength: 128 }
|
|
13
18
|
},
|
|
14
19
|
required: ['username', 'password']
|
|
15
20
|
}
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
export const
|
|
23
|
+
export const RESET_SEND_SCHEMA = {
|
|
19
24
|
body: {
|
|
20
25
|
type: 'object',
|
|
21
26
|
properties: {
|
|
22
|
-
email: {
|
|
23
|
-
|
|
27
|
+
email: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
|
|
30
|
+
minLength: 3,
|
|
31
|
+
maxLength: 254
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
required: ['email']
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const RESET_CALL_SCHEMA = {
|
|
39
|
+
body: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
email: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
|
|
45
|
+
minLength: 3,
|
|
46
|
+
maxLength: 254
|
|
47
|
+
},
|
|
48
|
+
password: { type: 'string', minLength: 8, maxLength: 128 },
|
|
49
|
+
arguments: { type: 'array' }
|
|
24
50
|
},
|
|
25
51
|
required: ['email', 'password']
|
|
26
52
|
}
|
|
@@ -30,7 +56,7 @@ export const CONFIRM_RESET_SCHEMA = {
|
|
|
30
56
|
body: {
|
|
31
57
|
type: 'object',
|
|
32
58
|
properties: {
|
|
33
|
-
password: { type: 'string' },
|
|
59
|
+
password: { type: 'string', minLength: 8, maxLength: 128 },
|
|
34
60
|
token: { type: 'string' },
|
|
35
61
|
tokenId: { type: 'string' }
|
|
36
62
|
},
|
|
@@ -38,12 +64,30 @@ export const CONFIRM_RESET_SCHEMA = {
|
|
|
38
64
|
}
|
|
39
65
|
}
|
|
40
66
|
|
|
67
|
+
export const CONFIRM_USER_SCHEMA = {
|
|
68
|
+
body: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
token: { type: 'string' },
|
|
72
|
+
tokenId: { type: 'string' }
|
|
73
|
+
},
|
|
74
|
+
required: ['token', 'tokenId']
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const RESET_SCHEMA = RESET_SEND_SCHEMA
|
|
79
|
+
|
|
41
80
|
export const REGISTRATION_SCHEMA = {
|
|
42
81
|
body: {
|
|
43
82
|
type: 'object',
|
|
44
83
|
properties: {
|
|
45
|
-
email: {
|
|
46
|
-
|
|
84
|
+
email: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
|
|
87
|
+
minLength: 3,
|
|
88
|
+
maxLength: 254
|
|
89
|
+
},
|
|
90
|
+
password: { type: 'string', minLength: 8, maxLength: 128 }
|
|
47
91
|
},
|
|
48
92
|
required: ['email', 'password']
|
|
49
93
|
}
|
|
@@ -52,9 +96,11 @@ export const REGISTRATION_SCHEMA = {
|
|
|
52
96
|
export enum AUTH_ENDPOINTS {
|
|
53
97
|
LOGIN = '/login',
|
|
54
98
|
REGISTRATION = '/register',
|
|
99
|
+
CONFIRM = '/confirm',
|
|
55
100
|
PROFILE = '/profile',
|
|
56
101
|
SESSION = '/session',
|
|
57
|
-
RESET = '/reset/
|
|
102
|
+
RESET = '/reset/send',
|
|
103
|
+
RESET_CALL = '/reset/call',
|
|
58
104
|
CONFIRM_RESET = "/reset",
|
|
59
105
|
FIRST_USER = '/setup/first-user'
|
|
60
106
|
}
|
|
@@ -62,7 +108,9 @@ export enum AUTH_ENDPOINTS {
|
|
|
62
108
|
export enum AUTH_ERRORS {
|
|
63
109
|
INVALID_CREDENTIALS = 'Invalid credentials',
|
|
64
110
|
INVALID_TOKEN = 'Invalid refresh token provided',
|
|
65
|
-
INVALID_RESET_PARAMS = 'Invalid token or tokenId provided'
|
|
111
|
+
INVALID_RESET_PARAMS = 'Invalid token or tokenId provided',
|
|
112
|
+
MISSING_RESET_FUNCTION = 'Missing reset function',
|
|
113
|
+
USER_NOT_CONFIRMED = 'User not confirmed'
|
|
66
114
|
}
|
|
67
115
|
|
|
68
116
|
export interface AuthConfig {
|
|
@@ -70,6 +118,7 @@ export interface AuthConfig {
|
|
|
70
118
|
'api-key': ApiKey
|
|
71
119
|
'local-userpass': LocalUserpass
|
|
72
120
|
'custom-function': CustomFunction
|
|
121
|
+
'anon-user'?: AnonUser
|
|
73
122
|
}
|
|
74
123
|
|
|
75
124
|
interface ApiKey {
|
|
@@ -93,17 +142,19 @@ interface CustomFunction {
|
|
|
93
142
|
}
|
|
94
143
|
}
|
|
95
144
|
|
|
145
|
+
export interface AnonUser {
|
|
146
|
+
name: "anon-user"
|
|
147
|
+
type: "anon-user"
|
|
148
|
+
disabled: boolean
|
|
149
|
+
}
|
|
150
|
+
|
|
96
151
|
export interface Config {
|
|
97
152
|
autoConfirm: boolean
|
|
153
|
+
confirmationFunctionName?: string
|
|
98
154
|
resetFunctionName: string
|
|
99
155
|
resetPasswordUrl: string
|
|
100
156
|
runConfirmationFunction: boolean
|
|
101
157
|
runResetFunction: boolean
|
|
102
|
-
mailConfig: {
|
|
103
|
-
from: string
|
|
104
|
-
subject: string
|
|
105
|
-
mailToken: string
|
|
106
|
-
}
|
|
107
158
|
}
|
|
108
159
|
|
|
109
160
|
export interface CustomUserDataConfig {
|
|
@@ -137,74 +188,6 @@ export const loadCustomUserData = (): CustomUserDataConfig => {
|
|
|
137
188
|
return JSON.parse(fs.readFileSync(userDataPath, 'utf-8'))
|
|
138
189
|
}
|
|
139
190
|
|
|
140
|
-
export const getMailConfig = (
|
|
141
|
-
resetPasswordConfig: Config,
|
|
142
|
-
token: string,
|
|
143
|
-
tokenId: string
|
|
144
|
-
) => {
|
|
145
|
-
const { mailConfig, resetPasswordUrl } = resetPasswordConfig
|
|
146
|
-
const ENV_PREFIX = 'ENV'
|
|
147
|
-
const { from, subject, mailToken } = mailConfig
|
|
148
|
-
|
|
149
|
-
const [fromPrefix, fromPath] = from.split('.')
|
|
150
|
-
|
|
151
|
-
if (!fromPath) {
|
|
152
|
-
throw new Error(`Invalid fromPath: ${fromPath}`)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const currentSender = (fromPrefix === ENV_PREFIX ? process.env[fromPath] : from) ?? ''
|
|
156
|
-
const [subjectPrefix, subjectPath] = subject.split('.')
|
|
157
|
-
|
|
158
|
-
if (!subjectPath) {
|
|
159
|
-
throw new Error(`Invalid subjectPath: ${subjectPath}`)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const currentSubject =
|
|
163
|
-
(subjectPrefix === ENV_PREFIX ? process.env[subjectPath] : subject) ?? ''
|
|
164
|
-
const [mailTokenPrefix, mailTokenPath] = mailToken.split('.')
|
|
165
|
-
|
|
166
|
-
if (!mailTokenPath) {
|
|
167
|
-
throw new Error(`Invalid mailTokenPath: ${mailTokenPath}`)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const currentMailToken =
|
|
171
|
-
(mailTokenPrefix === 'ENV' ? process.env[mailTokenPath] : mailToken) ?? ''
|
|
172
|
-
|
|
173
|
-
const link = `${resetPasswordUrl}/${token}/${tokenId}`
|
|
174
|
-
const body = `<body style="font-family: Arial, sans-serif; background-color: #f4f4f4; text-align: center; padding: 20px;">
|
|
175
|
-
<table width="100%" cellspacing="0" cellpadding="0">
|
|
176
|
-
<tr>
|
|
177
|
-
<td align="center">
|
|
178
|
-
<table width="600" cellspacing="0" cellpadding="0" style="background: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);">
|
|
179
|
-
<tr>
|
|
180
|
-
<td align="center">
|
|
181
|
-
<h2>Password Reset Request</h2>
|
|
182
|
-
<p>If you requested a password reset, click the button below to reset your password.</p>
|
|
183
|
-
<p>If you did not request this, please ignore this email.</p>
|
|
184
|
-
<p>
|
|
185
|
-
<a href="${link}" style="display: inline-block; padding: 12px 20px; font-size: 16px; color: #ffffff; background: #007bff; text-decoration: none; border-radius: 5px;">Reset Password</a>
|
|
186
|
-
</p>
|
|
187
|
-
<p style="margin-top: 20px; font-size: 12px; color: #777;">If the button does not work, copy and paste the following link into your browser:</p>
|
|
188
|
-
<p style="font-size: 12px; color: #777;">${link}</p>
|
|
189
|
-
</td>
|
|
190
|
-
</tr>
|
|
191
|
-
</table>
|
|
192
|
-
</td>
|
|
193
|
-
</tr>
|
|
194
|
-
</table>
|
|
195
|
-
</body>`
|
|
196
|
-
return {
|
|
197
|
-
from: currentSender ?? '',
|
|
198
|
-
subject: currentSubject,
|
|
199
|
-
mailToken: currentMailToken,
|
|
200
|
-
body
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
191
|
export const generatePassword = (length = 20) => {
|
|
209
192
|
const bytes = crypto.randomBytes(length);
|
|
210
193
|
return Array.from(bytes, (b) => CHARSET[b % CHARSET.length]).join("");
|
package/src/constants.ts
CHANGED
|
@@ -17,6 +17,15 @@ export const DEFAULT_CONFIG = {
|
|
|
17
17
|
HTTPS_SCHEMA: process.env.HTTPS_SCHEMA || 'https',
|
|
18
18
|
HOST: process.env.HOST || '0.0.0.0',
|
|
19
19
|
ENABLE_LOGGER: process.env.ENABLE_LOGGER,
|
|
20
|
+
RESET_PASSWORD_TTL_SECONDS: Number(process.env.RESET_PASSWORD_TTL_SECONDS) || 3600,
|
|
21
|
+
AUTH_RATE_LIMIT_WINDOW_MS: Number(process.env.AUTH_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
|
22
|
+
AUTH_LOGIN_MAX_ATTEMPTS: Number(process.env.AUTH_LOGIN_MAX_ATTEMPTS) || 10,
|
|
23
|
+
AUTH_REGISTER_MAX_ATTEMPTS: Number(process.env.AUTH_REGISTER_MAX_ATTEMPTS) || 5,
|
|
24
|
+
AUTH_RESET_MAX_ATTEMPTS: Number(process.env.AUTH_RESET_MAX_ATTEMPTS) || 5,
|
|
25
|
+
REFRESH_TOKEN_TTL_DAYS: Number(process.env.REFRESH_TOKEN_TTL_DAYS) || 60,
|
|
26
|
+
ANON_USER_TTL_SECONDS: Number(process.env.ANON_USER_TTL_SECONDS) || 3 * 60 * 60,
|
|
27
|
+
SWAGGER_UI_USER: process.env.SWAGGER_UI_USER || '',
|
|
28
|
+
SWAGGER_UI_PASSWORD: process.env.SWAGGER_UI_PASSWORD || '',
|
|
20
29
|
CORS_OPTIONS: {
|
|
21
30
|
origin: "*",
|
|
22
31
|
methods: ["GET", "POST", "PUT", "DELETE"] as ALLOWED_METHODS[]
|
|
@@ -30,12 +39,15 @@ export const DB_NAME = database_name
|
|
|
30
39
|
export const AUTH_CONFIG = {
|
|
31
40
|
authCollection: auth_collection,
|
|
32
41
|
userCollection: collection_name,
|
|
33
|
-
resetPasswordCollection: '
|
|
42
|
+
resetPasswordCollection: 'reset_password_requests',
|
|
43
|
+
refreshTokensCollection: 'auth_refresh_tokens',
|
|
34
44
|
resetPasswordConfig: configuration['local-userpass']?.config,
|
|
45
|
+
localUserpassConfig: configuration['local-userpass']?.config,
|
|
35
46
|
user_id_field,
|
|
36
47
|
on_user_creation_function_name,
|
|
37
48
|
providers: {
|
|
38
|
-
"custom-function": configuration['custom-function']?.config
|
|
49
|
+
"custom-function": configuration['custom-function']?.config,
|
|
50
|
+
"anon-user": configuration['anon-user']
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
|