@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.
- package/README.md +28 -3
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +57 -3
- package/dist/auth/plugins/jwt.d.ts.map +1 -1
- package/dist/auth/plugins/jwt.js +49 -3
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +19 -3
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +125 -71
- package/dist/auth/providers/local-userpass/dtos.d.ts +11 -2
- package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
- package/dist/auth/utils.d.ts +53 -14
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +46 -63
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +18 -5
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +32 -3
- 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 +2 -1
- package/dist/features/functions/utils.d.ts.map +1 -1
- package/dist/features/functions/utils.js +19 -7
- package/dist/features/rules/utils.d.ts.map +1 -1
- package/dist/features/rules/utils.js +11 -2
- package/dist/features/triggers/index.d.ts.map +1 -1
- package/dist/features/triggers/index.js +48 -7
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +118 -27
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +57 -21
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +605 -478
- package/dist/services/mongodb-atlas/model.d.ts +2 -1
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.d.ts +9 -2
- package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.js +113 -23
- package/dist/shared/handleUserRegistration.d.ts.map +1 -1
- package/dist/shared/handleUserRegistration.js +4 -1
- package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
- package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
- package/dist/utils/context/helpers.d.ts +7 -6
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/context/helpers.js +3 -0
- 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 -5
- 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.d.ts.map +1 -1
- package/dist/utils/initializer/exposeRoutes.js +11 -4
- package/dist/utils/initializer/registerPlugins.d.ts +3 -1
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +9 -6
- package/dist/utils/roles/helpers.js +11 -3
- package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
- package/dist/utils/roles/machines/commonValidators.js +10 -6
- package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
- package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/B/validators.js +8 -0
- package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/C/index.js +10 -7
- package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
- package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/C/validators.js +29 -0
- package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/D/index.js +13 -11
- package/dist/utils/rules.d.ts +1 -1
- package/dist/utils/rules.d.ts.map +1 -1
- package/dist/utils/rules.js +26 -17
- package/jest.config.ts +2 -12
- package/jest.setup.ts +28 -0
- package/package.json +1 -2
- package/src/auth/controller.ts +70 -4
- package/src/auth/plugins/jwt.test.ts +93 -0
- package/src/auth/plugins/jwt.ts +62 -3
- package/src/auth/providers/custom-function/controller.ts +22 -5
- package/src/auth/providers/local-userpass/controller.ts +168 -96
- package/src/auth/providers/local-userpass/dtos.ts +13 -2
- package/src/auth/utils.ts +51 -86
- package/src/constants.ts +17 -3
- package/src/fastify.d.ts +32 -15
- package/src/features/functions/controller.ts +51 -3
- 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/rules/utils.ts +11 -2
- package/src/features/triggers/index.ts +43 -1
- package/src/features/triggers/utils.ts +146 -38
- package/src/index.ts +69 -20
- package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
- package/src/services/mongodb-atlas/index.ts +241 -90
- package/src/services/mongodb-atlas/model.ts +15 -2
- package/src/services/mongodb-atlas/utils.ts +158 -22
- package/src/shared/handleUserRegistration.ts +5 -4
- package/src/shared/models/handleUserRegistration.model.ts +8 -3
- package/src/types/fastify-raw-body.d.ts +22 -0
- package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
- package/src/utils/__tests__/registerPlugins.test.ts +16 -1
- package/src/utils/context/helpers.ts +3 -0
- package/src/utils/context/index.ts +238 -13
- package/src/utils/context/interface.ts +1 -1
- package/src/utils/crypto/index.ts +5 -1
- package/src/utils/initializer/exposeRoutes.ts +15 -8
- package/src/utils/initializer/registerPlugins.ts +15 -7
- package/src/utils/roles/helpers.ts +23 -5
- package/src/utils/roles/machines/commonValidators.ts +10 -5
- package/src/utils/roles/machines/read/B/validators.ts +8 -0
- package/src/utils/roles/machines/read/C/index.ts +11 -7
- package/src/utils/roles/machines/read/C/validators.ts +21 -0
- package/src/utils/roles/machines/read/D/index.ts +22 -12
- package/src/utils/rules.ts +31 -22
- 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
|
+
})
|
package/src/auth/plugins/jwt.ts
CHANGED
|
@@ -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
|
-
|
|
29
|
-
reply.send(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = {
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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:
|
|
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/
|
|
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<
|
|
236
|
+
app.post<ResetPasswordSendDto>(
|
|
163
237
|
AUTH_ENDPOINTS.RESET,
|
|
164
238
|
{
|
|
165
|
-
schema:
|
|
239
|
+
schema: RESET_SEND_SCHEMA
|
|
166
240
|
},
|
|
167
|
-
async function (req) {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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
|
|
37
|
+
export interface ResetPasswordCallDto {
|
|
28
38
|
Body: {
|
|
29
39
|
email: string
|
|
30
40
|
password: string
|
|
41
|
+
arguments?: unknown[]
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
|