@flowerforce/flowerbase 1.2.1-beta.2 → 1.2.1-beta.20
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/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/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 +100 -2
- package/src/services/mongodb-atlas/model.ts +16 -3
- 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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { FastifyInstance } from 'fastify'
|
|
2
|
+
import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
|
|
3
|
+
import { hashToken } from '../../../utils/crypto'
|
|
4
|
+
import { PROVIDER } from '../../../shared/models/handleUserRegistration.model'
|
|
5
|
+
import { AUTH_ENDPOINTS } from '../../utils'
|
|
6
|
+
import { LoginDto } from './dtos'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Controller for handling anonymous user login.
|
|
10
|
+
* @testable
|
|
11
|
+
* @param {FastifyInstance} app - The Fastify instance.
|
|
12
|
+
*/
|
|
13
|
+
export async function anonUserController(app: FastifyInstance) {
|
|
14
|
+
const db = app.mongo.client.db(DB_NAME)
|
|
15
|
+
const { authCollection, refreshTokensCollection, providers } = AUTH_CONFIG
|
|
16
|
+
const refreshTokenTtlMs = DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000
|
|
17
|
+
const anonUserTtlSeconds = DEFAULT_CONFIG.ANON_USER_TTL_SECONDS
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await db.collection(authCollection!).createIndex(
|
|
21
|
+
{ createdAt: 1 },
|
|
22
|
+
{
|
|
23
|
+
expireAfterSeconds: anonUserTtlSeconds,
|
|
24
|
+
partialFilterExpression: { 'identities.provider_type': PROVIDER.ANON_USER }
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Failed to ensure anonymous user TTL index', error)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
app.post<LoginDto>(
|
|
32
|
+
AUTH_ENDPOINTS.LOGIN,
|
|
33
|
+
async function () {
|
|
34
|
+
const anonProvider = providers?.['anon-user']
|
|
35
|
+
if (anonProvider?.disabled) {
|
|
36
|
+
throw new Error('Anonymous authentication disabled')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const now = new Date()
|
|
40
|
+
const insertResult = await db.collection(authCollection!).insertOne({
|
|
41
|
+
status: 'confirmed',
|
|
42
|
+
createdAt: now,
|
|
43
|
+
custom_data: {},
|
|
44
|
+
identities: [
|
|
45
|
+
{
|
|
46
|
+
provider_type: PROVIDER.ANON_USER,
|
|
47
|
+
provider_data: {}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const userId = insertResult.insertedId
|
|
53
|
+
await db.collection(authCollection!).updateOne(
|
|
54
|
+
{ _id: userId },
|
|
55
|
+
{
|
|
56
|
+
$set: {
|
|
57
|
+
identities: [
|
|
58
|
+
{
|
|
59
|
+
id: userId.toString(),
|
|
60
|
+
provider_id: userId.toString(),
|
|
61
|
+
provider_type: PROVIDER.ANON_USER,
|
|
62
|
+
provider_data: {}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const currentUserData = {
|
|
70
|
+
_id: userId,
|
|
71
|
+
user_data: {}
|
|
72
|
+
}
|
|
73
|
+
const refreshToken = this.createRefreshToken(currentUserData)
|
|
74
|
+
const refreshTokenHash = hashToken(refreshToken)
|
|
75
|
+
await db.collection(refreshTokensCollection).insertOne({
|
|
76
|
+
userId,
|
|
77
|
+
tokenHash: refreshTokenHash,
|
|
78
|
+
createdAt: now,
|
|
79
|
+
expiresAt: new Date(Date.now() + refreshTokenTtlMs),
|
|
80
|
+
revokedAt: null
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
access_token: this.createAccessToken(currentUserData),
|
|
85
|
+
refresh_token: refreshToken,
|
|
86
|
+
device_id: '',
|
|
87
|
+
user_id: userId.toString()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { FastifyInstance } from 'fastify'
|
|
2
|
-
import { AUTH_CONFIG } from '../../../constants'
|
|
3
|
-
import handleUserRegistration from '../../../shared/handleUserRegistration'
|
|
4
|
-
import { PROVIDER } from '../../../shared/models/handleUserRegistration.model'
|
|
2
|
+
import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
|
|
5
3
|
import { StateManager } from '../../../state'
|
|
6
4
|
import { GenerateContext } from '../../../utils/context'
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from '../../utils'
|
|
11
|
-
import {
|
|
12
|
-
LoginDto
|
|
13
|
-
} from './dtos'
|
|
5
|
+
import { hashToken } from '../../../utils/crypto'
|
|
6
|
+
import { AUTH_ENDPOINTS } from '../../utils'
|
|
7
|
+
import { LoginDto } from './dtos'
|
|
14
8
|
import { LOGIN_SCHEMA } from './schema'
|
|
15
9
|
|
|
16
10
|
/**
|
|
@@ -22,6 +16,9 @@ export async function customFunctionController(app: FastifyInstance) {
|
|
|
22
16
|
|
|
23
17
|
const functionsList = StateManager.select('functions')
|
|
24
18
|
const services = StateManager.select('services')
|
|
19
|
+
const db = app.mongo.client.db(DB_NAME)
|
|
20
|
+
const { authCollection, refreshTokensCollection } = AUTH_CONFIG
|
|
21
|
+
const refreshTokenTtlMs = DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000
|
|
25
22
|
|
|
26
23
|
/**
|
|
27
24
|
* Endpoint for user login.
|
|
@@ -35,7 +32,7 @@ export async function customFunctionController(app: FastifyInstance) {
|
|
|
35
32
|
{
|
|
36
33
|
schema: LOGIN_SCHEMA
|
|
37
34
|
},
|
|
38
|
-
async function (req) {
|
|
35
|
+
async function (req, reply) {
|
|
39
36
|
const { providers } = AUTH_CONFIG
|
|
40
37
|
const authFunctionName = providers["custom-function"].authFunctionName
|
|
41
38
|
|
|
@@ -53,7 +50,8 @@ export async function customFunctionController(app: FastifyInstance) {
|
|
|
53
50
|
id
|
|
54
51
|
} = req
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
type CustomFunctionAuthResult = { id?: string }
|
|
54
|
+
const authResult = await GenerateContext({
|
|
57
55
|
args: [
|
|
58
56
|
req.body
|
|
59
57
|
],
|
|
@@ -72,30 +70,41 @@ export async function customFunctionController(app: FastifyInstance) {
|
|
|
72
70
|
ip,
|
|
73
71
|
id
|
|
74
72
|
}
|
|
75
|
-
})
|
|
73
|
+
}) as CustomFunctionAuthResult
|
|
76
74
|
|
|
77
75
|
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
76
|
+
if (!authResult.id) {
|
|
77
|
+
reply.code(401).send({ message: 'Unauthorized' })
|
|
78
|
+
return
|
|
79
|
+
}
|
|
83
80
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
access_token: this.createAccessToken(currentUserData),
|
|
92
|
-
refresh_token: this.createRefreshToken(currentUserData),
|
|
93
|
-
device_id: '',
|
|
94
|
-
user_id: user.insertedId.toString()
|
|
95
|
-
}
|
|
81
|
+
const authUser = await db.collection(authCollection!).findOne({ email: authResult.id })
|
|
82
|
+
if (!authUser) {
|
|
83
|
+
reply.code(401).send({ message: 'Unauthorized' })
|
|
84
|
+
return
|
|
96
85
|
}
|
|
97
86
|
|
|
98
|
-
|
|
87
|
+
const currentUserData = {
|
|
88
|
+
_id: authUser._id,
|
|
89
|
+
user_data: {
|
|
90
|
+
_id: authUser._id
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const refreshToken = this.createRefreshToken(currentUserData)
|
|
94
|
+
const refreshTokenHash = hashToken(refreshToken)
|
|
95
|
+
await db.collection(refreshTokensCollection).insertOne({
|
|
96
|
+
userId: authUser._id,
|
|
97
|
+
tokenHash: refreshTokenHash,
|
|
98
|
+
createdAt: new Date(),
|
|
99
|
+
expiresAt: new Date(Date.now() + refreshTokenTtlMs),
|
|
100
|
+
revokedAt: null
|
|
101
|
+
})
|
|
102
|
+
return {
|
|
103
|
+
access_token: this.createAccessToken(currentUserData),
|
|
104
|
+
refresh_token: refreshToken,
|
|
105
|
+
device_id: '',
|
|
106
|
+
user_id: authUser._id.toString()
|
|
107
|
+
}
|
|
99
108
|
}
|
|
100
109
|
)
|
|
101
110
|
|
|
@@ -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
|
+
}
|