@flowerforce/flowerbase 1.7.6-beta.1 → 1.7.6-beta.10
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/dist/auth/providers/anon-user/controller.d.ts.map +1 -1
- package/dist/auth/providers/anon-user/controller.js +1 -0
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +3 -1
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +58 -18
- package/dist/auth/providers/local-userpass/dtos.d.ts +5 -1
- package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
- package/dist/auth/utils.d.ts +1 -0
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +1 -0
- package/dist/features/endpoints/utils.d.ts.map +1 -1
- package/dist/features/endpoints/utils.js +3 -0
- package/dist/features/functions/controller.d.ts +2 -0
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +7 -1
- package/dist/features/rules/interface.d.ts +6 -5
- package/dist/features/rules/interface.d.ts.map +1 -1
- package/dist/features/triggers/interface.d.ts +1 -1
- 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 +60 -0
- package/dist/services/mongodb-atlas/index.d.ts +3 -0
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +128 -37
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/context/helpers.js +3 -2
- package/dist/utils/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +4 -2
- package/dist/utils/roles/helpers.d.ts.map +1 -1
- package/dist/utils/roles/helpers.js +6 -3
- package/dist/utils/roles/machines/fieldPermissions.d.ts.map +1 -1
- package/dist/utils/roles/machines/fieldPermissions.js +19 -10
- package/dist/utils/rules-matcher/interface.d.ts +2 -0
- package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
- package/dist/utils/rules-matcher/interface.js +1 -0
- package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
- package/dist/utils/rules-matcher/utils.js +23 -6
- package/package.json +1 -1
- package/src/auth/providers/anon-user/controller.ts +1 -0
- package/src/auth/providers/custom-function/controller.ts +6 -1
- package/src/auth/providers/local-userpass/__tests__/controller.test.ts +200 -0
- package/src/auth/providers/local-userpass/controller.ts +87 -34
- package/src/auth/providers/local-userpass/dtos.ts +6 -1
- package/src/auth/utils.ts +1 -0
- package/src/features/endpoints/__tests__/utils.test.ts +65 -0
- package/src/features/endpoints/utils.ts +3 -0
- package/src/features/functions/__tests__/watch-filter.test.ts +11 -1
- package/src/features/functions/controller.ts +8 -0
- package/src/features/rules/interface.ts +18 -17
- package/src/features/triggers/__tests__/index.test.ts +6 -4
- package/src/features/triggers/interface.ts +1 -1
- package/src/features/triggers/utils.ts +60 -0
- package/src/monitoring/ui.events.js +1 -1
- package/src/services/mongodb-atlas/__tests__/realmCompatibility.test.ts +205 -7
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +27 -0
- package/src/services/mongodb-atlas/__tests__/watch-filter.test.ts +78 -0
- package/src/services/mongodb-atlas/index.ts +371 -171
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +74 -5
- package/src/utils/__tests__/contextExecuteCompatibility.test.ts +27 -1
- package/src/utils/__tests__/evaluateExpression.test.ts +33 -0
- package/src/utils/__tests__/generateContextData.test.ts +5 -1
- package/src/utils/__tests__/rule.test.ts +38 -0
- package/src/utils/context/helpers.ts +3 -2
- package/src/utils/context/index.ts +4 -3
- package/src/utils/roles/helpers.ts +10 -5
- package/src/utils/roles/machines/fieldPermissions.ts +17 -8
- package/src/utils/rules-matcher/interface.ts +2 -0
- package/src/utils/rules-matcher/utils.ts +33 -17
- package/src/utils/__tests__/readFileContent.test.ts +0 -35
|
@@ -128,15 +128,20 @@ export async function customFunctionController(app: FastifyInstance) {
|
|
|
128
128
|
...(user || {})
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
const now = new Date()
|
|
131
132
|
const refreshToken = this.createRefreshToken(currentUserData)
|
|
132
133
|
const refreshTokenHash = hashToken(refreshToken)
|
|
133
134
|
await authDb.collection(refreshTokensCollection).insertOne({
|
|
134
135
|
userId: authUser._id,
|
|
135
136
|
tokenHash: refreshTokenHash,
|
|
136
|
-
createdAt:
|
|
137
|
+
createdAt: now,
|
|
137
138
|
expiresAt: new Date(Date.now() + refreshTokenTtlMs),
|
|
138
139
|
revokedAt: null
|
|
139
140
|
})
|
|
141
|
+
await authDb.collection(authCollection!).updateOne(
|
|
142
|
+
{ _id: authUser._id },
|
|
143
|
+
{ $set: { lastLoginAt: now } }
|
|
144
|
+
)
|
|
140
145
|
return {
|
|
141
146
|
access_token: this.createAccessToken(currentUserData),
|
|
142
147
|
refresh_token: refreshToken,
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
jest.mock('../../../../constants', () => ({
|
|
2
|
+
AUTH_CONFIG: {
|
|
3
|
+
authCollection: 'auth_users',
|
|
4
|
+
refreshTokensCollection: 'refresh_tokens',
|
|
5
|
+
resetPasswordCollection: 'reset_password_requests',
|
|
6
|
+
userCollection: 'users',
|
|
7
|
+
user_id_field: 'id',
|
|
8
|
+
authProviders: {
|
|
9
|
+
'local-userpass': {
|
|
10
|
+
disabled: false
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
resetPasswordConfig: {
|
|
14
|
+
runResetFunction: true,
|
|
15
|
+
resetFunctionName: 'customReset'
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
AUTH_DB_NAME: 'test-auth-db',
|
|
19
|
+
DB_NAME: 'test-db',
|
|
20
|
+
DEFAULT_CONFIG: {
|
|
21
|
+
RESET_PASSWORD_TTL_SECONDS: 3600,
|
|
22
|
+
AUTH_RATE_LIMIT_WINDOW_MS: 60000,
|
|
23
|
+
AUTH_LOGIN_MAX_ATTEMPTS: 5,
|
|
24
|
+
AUTH_REGISTER_MAX_ATTEMPTS: 5,
|
|
25
|
+
AUTH_RESET_MAX_ATTEMPTS: 5,
|
|
26
|
+
REFRESH_TOKEN_TTL_DAYS: 1
|
|
27
|
+
}
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
jest.mock('../../../../state', () => ({
|
|
31
|
+
StateManager: {
|
|
32
|
+
select: jest.fn((key: string) => {
|
|
33
|
+
if (key === 'functions') {
|
|
34
|
+
return {
|
|
35
|
+
customReset: { name: 'customReset', code: 'exports = async () => ({ status: "success" })' }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (key === 'services') {
|
|
39
|
+
return {}
|
|
40
|
+
}
|
|
41
|
+
return {}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
jest.mock('../../../../utils/context', () => ({
|
|
47
|
+
GenerateContext: jest.fn()
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
jest.mock('../../../../utils/crypto', () => ({
|
|
51
|
+
comparePassword: jest.fn(),
|
|
52
|
+
generateToken: jest.fn(() => 'generated-token'),
|
|
53
|
+
hashPassword: jest.fn(async (password: string) => `hashed:${password}`),
|
|
54
|
+
hashToken: jest.fn(() => 'hashed-token')
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
import { AUTH_ERRORS } from '../../../utils'
|
|
58
|
+
import { localUserPassController } from '../controller'
|
|
59
|
+
import { GenerateContext } from '../../../../utils/context'
|
|
60
|
+
import { hashPassword } from '../../../../utils/crypto'
|
|
61
|
+
|
|
62
|
+
describe('localUserPassController reset call', () => {
|
|
63
|
+
const buildApp = () => {
|
|
64
|
+
let resetCallHandler:
|
|
65
|
+
| ((req: { body: { email: string; password: string; arguments?: unknown[] }; ip: string }, res: { status: jest.Mock }) => Promise<unknown>)
|
|
66
|
+
| undefined
|
|
67
|
+
|
|
68
|
+
const authUsersCollection = {
|
|
69
|
+
findOne: jest.fn().mockResolvedValue({
|
|
70
|
+
_id: 'auth-user-1',
|
|
71
|
+
email: 'john@doe.com',
|
|
72
|
+
password: 'old-hash'
|
|
73
|
+
}),
|
|
74
|
+
updateOne: jest.fn().mockResolvedValue({ acknowledged: true })
|
|
75
|
+
}
|
|
76
|
+
const resetCollection = {
|
|
77
|
+
createIndex: jest.fn().mockResolvedValue('ok'),
|
|
78
|
+
updateOne: jest.fn().mockResolvedValue({ acknowledged: true }),
|
|
79
|
+
deleteOne: jest.fn().mockResolvedValue({ acknowledged: true }),
|
|
80
|
+
findOne: jest.fn()
|
|
81
|
+
}
|
|
82
|
+
const refreshCollection = {
|
|
83
|
+
createIndex: jest.fn().mockResolvedValue('ok'),
|
|
84
|
+
insertOne: jest.fn()
|
|
85
|
+
}
|
|
86
|
+
const usersCollection = {
|
|
87
|
+
findOne: jest.fn()
|
|
88
|
+
}
|
|
89
|
+
const db = {
|
|
90
|
+
collection: jest.fn((name: string) => {
|
|
91
|
+
if (name === 'auth_users') return authUsersCollection
|
|
92
|
+
if (name === 'reset_password_requests') return resetCollection
|
|
93
|
+
if (name === 'refresh_tokens') return refreshCollection
|
|
94
|
+
if (name === 'users') return usersCollection
|
|
95
|
+
return {}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
const app = {
|
|
99
|
+
mongo: { client: { db: jest.fn().mockReturnValue(db) } },
|
|
100
|
+
post: jest.fn((path: string, _opts: unknown, handler: typeof resetCallHandler) => {
|
|
101
|
+
if (path === '/reset/call') {
|
|
102
|
+
resetCallHandler = handler
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { app, authUsersCollection, resetCollection, resetCallHandlerRef: () => resetCallHandler }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
jest.clearAllMocks()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('hashes and applies the password when the custom reset function returns success', async () => {
|
|
115
|
+
;(GenerateContext as jest.Mock).mockResolvedValue({ status: 'success' })
|
|
116
|
+
const { app, authUsersCollection, resetCollection, resetCallHandlerRef } = buildApp()
|
|
117
|
+
|
|
118
|
+
await localUserPassController(app as never)
|
|
119
|
+
|
|
120
|
+
const res = { status: jest.fn() }
|
|
121
|
+
const result = await resetCallHandlerRef()?.(
|
|
122
|
+
{
|
|
123
|
+
body: { email: 'john@doe.com', password: 'new-secret', arguments: ['extra'] },
|
|
124
|
+
ip: '127.0.0.1'
|
|
125
|
+
},
|
|
126
|
+
res
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
expect(GenerateContext).toHaveBeenCalledWith(expect.objectContaining({
|
|
130
|
+
args: [
|
|
131
|
+
{
|
|
132
|
+
token: 'generated-token',
|
|
133
|
+
tokenId: 'generated-token',
|
|
134
|
+
email: 'john@doe.com',
|
|
135
|
+
password: 'new-secret',
|
|
136
|
+
username: 'john@doe.com'
|
|
137
|
+
},
|
|
138
|
+
'extra'
|
|
139
|
+
],
|
|
140
|
+
runAsSystem: true
|
|
141
|
+
}))
|
|
142
|
+
expect(hashPassword).toHaveBeenCalledWith('new-secret')
|
|
143
|
+
expect(authUsersCollection.updateOne).toHaveBeenCalledWith(
|
|
144
|
+
{ email: 'john@doe.com' },
|
|
145
|
+
{ $set: { password: 'hashed:new-secret' } }
|
|
146
|
+
)
|
|
147
|
+
expect(resetCollection.deleteOne).toHaveBeenCalledWith({ email: 'john@doe.com' })
|
|
148
|
+
expect(res.status).toHaveBeenCalledWith(202)
|
|
149
|
+
expect(result).toEqual({ status: 'success' })
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('returns pending without changing the password when the custom reset function returns pending', async () => {
|
|
153
|
+
;(GenerateContext as jest.Mock).mockResolvedValue({ status: 'pending' })
|
|
154
|
+
const { app, authUsersCollection, resetCollection, resetCallHandlerRef } = buildApp()
|
|
155
|
+
|
|
156
|
+
await localUserPassController(app as never)
|
|
157
|
+
|
|
158
|
+
const res = { status: jest.fn() }
|
|
159
|
+
const result = await resetCallHandlerRef()?.(
|
|
160
|
+
{
|
|
161
|
+
body: { email: 'john@doe.com', password: 'new-secret' },
|
|
162
|
+
ip: '127.0.0.1'
|
|
163
|
+
},
|
|
164
|
+
res
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
expect(hashPassword).not.toHaveBeenCalled()
|
|
168
|
+
expect(authUsersCollection.updateOne).not.toHaveBeenCalledWith(
|
|
169
|
+
{ email: 'john@doe.com' },
|
|
170
|
+
expect.objectContaining({ $set: { password: expect.any(String) } })
|
|
171
|
+
)
|
|
172
|
+
expect(resetCollection.deleteOne).not.toHaveBeenCalled()
|
|
173
|
+
expect(res.status).toHaveBeenCalledWith(202)
|
|
174
|
+
expect(result).toEqual({ status: 'pending' })
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('rejects the request when the custom reset function returns fail', async () => {
|
|
178
|
+
;(GenerateContext as jest.Mock).mockResolvedValue({ status: 'fail' })
|
|
179
|
+
const { app, authUsersCollection, resetCollection, resetCallHandlerRef } = buildApp()
|
|
180
|
+
|
|
181
|
+
await localUserPassController(app as never)
|
|
182
|
+
|
|
183
|
+
const res = { status: jest.fn() }
|
|
184
|
+
|
|
185
|
+
await expect(
|
|
186
|
+
resetCallHandlerRef()?.(
|
|
187
|
+
{
|
|
188
|
+
body: { email: 'john@doe.com', password: 'new-secret' },
|
|
189
|
+
ip: '127.0.0.1'
|
|
190
|
+
},
|
|
191
|
+
res
|
|
192
|
+
)
|
|
193
|
+
).rejects.toThrow(AUTH_ERRORS.INVALID_RESET_PARAMS)
|
|
194
|
+
|
|
195
|
+
expect(hashPassword).not.toHaveBeenCalled()
|
|
196
|
+
expect(authUsersCollection.updateOne).not.toHaveBeenCalled()
|
|
197
|
+
expect(resetCollection.deleteOne).not.toHaveBeenCalled()
|
|
198
|
+
expect(res.status).not.toHaveBeenCalled()
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -5,7 +5,12 @@ import handleUserRegistration from '../../../shared/handleUserRegistration'
|
|
|
5
5
|
import { PROVIDER } from '../../../shared/models/handleUserRegistration.model'
|
|
6
6
|
import { StateManager } from '../../../state'
|
|
7
7
|
import { GenerateContext } from '../../../utils/context'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
comparePassword,
|
|
10
|
+
generateToken,
|
|
11
|
+
hashPassword,
|
|
12
|
+
hashToken
|
|
13
|
+
} from '../../../utils/crypto'
|
|
9
14
|
import {
|
|
10
15
|
AUTH_ENDPOINTS,
|
|
11
16
|
AUTH_ERRORS,
|
|
@@ -27,6 +32,8 @@ import {
|
|
|
27
32
|
|
|
28
33
|
const rateLimitStore = new Map<string, number[]>()
|
|
29
34
|
|
|
35
|
+
type ResetFunctionResult = { status?: 'success' | 'pending' | 'fail' }
|
|
36
|
+
|
|
30
37
|
const isRateLimited = (key: string, maxAttempts: number, windowMs: number) => {
|
|
31
38
|
const now = Date.now()
|
|
32
39
|
const existing = rateLimitStore.get(key) ?? []
|
|
@@ -53,21 +60,23 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
53
60
|
const resetMaxAttempts = DEFAULT_CONFIG.AUTH_RESET_MAX_ATTEMPTS
|
|
54
61
|
const refreshTokenTtlMs = DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000
|
|
55
62
|
const resolveLocalUserpassProvider = () => AUTH_CONFIG.authProviders?.['local-userpass']
|
|
63
|
+
const invalidPasswordError = {
|
|
64
|
+
error: 'unauthorized',
|
|
65
|
+
error_code: 'InvalidPassword'
|
|
66
|
+
} as const
|
|
56
67
|
|
|
57
68
|
try {
|
|
58
|
-
await authDb
|
|
59
|
-
|
|
60
|
-
{ expireAfterSeconds: resetPasswordTtlSeconds }
|
|
61
|
-
)
|
|
69
|
+
await authDb
|
|
70
|
+
.collection(resetPasswordCollection)
|
|
71
|
+
.createIndex({ createdAt: 1 }, { expireAfterSeconds: resetPasswordTtlSeconds })
|
|
62
72
|
} catch (error) {
|
|
63
73
|
console.error('Failed to ensure reset password TTL index', error)
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
try {
|
|
67
|
-
await authDb
|
|
68
|
-
|
|
69
|
-
{ expireAfterSeconds: 0 }
|
|
70
|
-
)
|
|
77
|
+
await authDb
|
|
78
|
+
.collection(refreshTokensCollection)
|
|
79
|
+
.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })
|
|
71
80
|
} catch (error) {
|
|
72
81
|
console.error('Failed to ensure refresh token TTL index', error)
|
|
73
82
|
}
|
|
@@ -105,8 +114,10 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
105
114
|
const services = StateManager.select('services')
|
|
106
115
|
const currentFunction = functionsList[resetPasswordConfig.resetFunctionName]
|
|
107
116
|
const baseArgs = { token, tokenId, email, password, username: email }
|
|
108
|
-
const args = Array.isArray(extraArguments)
|
|
109
|
-
|
|
117
|
+
const args = Array.isArray(extraArguments)
|
|
118
|
+
? [baseArgs, ...extraArguments]
|
|
119
|
+
: [baseArgs]
|
|
120
|
+
const response = (await GenerateContext({
|
|
110
121
|
args,
|
|
111
122
|
app,
|
|
112
123
|
rules: {},
|
|
@@ -114,11 +125,41 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
114
125
|
currentFunction,
|
|
115
126
|
functionName: resetPasswordConfig.resetFunctionName,
|
|
116
127
|
functionsList,
|
|
117
|
-
services
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
services,
|
|
129
|
+
runAsSystem: true
|
|
130
|
+
})) as ResetFunctionResult
|
|
131
|
+
const resetStatus = response?.status
|
|
132
|
+
|
|
133
|
+
if (resetStatus === 'success') {
|
|
134
|
+
if (!password) {
|
|
135
|
+
throw new Error(AUTH_ERRORS.INVALID_RESET_FUNCTION_RESPONSE)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const hashedPassword = await hashPassword(password)
|
|
139
|
+
await authDb.collection(authCollection!).updateOne(
|
|
140
|
+
{ email },
|
|
141
|
+
{
|
|
142
|
+
$set: {
|
|
143
|
+
password: hashedPassword
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
await authDb?.collection(resetPasswordCollection).deleteOne({ email })
|
|
148
|
+
return { status: 'success' as const }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (resetStatus === 'pending') {
|
|
152
|
+
return { status: 'pending' as const }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (resetStatus === 'fail') {
|
|
156
|
+
throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throw new Error(AUTH_ERRORS.INVALID_RESET_FUNCTION_RESPONSE)
|
|
120
160
|
}
|
|
121
161
|
|
|
162
|
+
return { status: 'pending' as const }
|
|
122
163
|
}
|
|
123
164
|
|
|
124
165
|
/**
|
|
@@ -147,12 +188,18 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
147
188
|
|
|
148
189
|
let result
|
|
149
190
|
try {
|
|
150
|
-
result = await handleUserRegistration(app, {
|
|
191
|
+
result = await handleUserRegistration(app, {
|
|
192
|
+
run_as_system: true,
|
|
193
|
+
provider: PROVIDER.LOCAL_USERPASS
|
|
194
|
+
})({
|
|
151
195
|
email: req.body.email.toLowerCase(),
|
|
152
196
|
password: req.body.password
|
|
153
197
|
})
|
|
154
198
|
} catch (error) {
|
|
155
|
-
if (
|
|
199
|
+
if (
|
|
200
|
+
error instanceof Error &&
|
|
201
|
+
error.message === 'This email address is already used'
|
|
202
|
+
) {
|
|
156
203
|
res.status(409).send({
|
|
157
204
|
error: 'name already in use',
|
|
158
205
|
error_code: 'AccountNameInUse'
|
|
@@ -195,10 +242,10 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
195
242
|
return
|
|
196
243
|
}
|
|
197
244
|
|
|
198
|
-
const existing = await authDb.collection(authCollection!).findOne({
|
|
245
|
+
const existing = (await authDb.collection(authCollection!).findOne({
|
|
199
246
|
confirmationToken: req.body.token,
|
|
200
247
|
confirmationTokenId: req.body.tokenId
|
|
201
|
-
}) as { _id: ObjectId; status?: string } | null
|
|
248
|
+
})) as { _id: ObjectId; status?: string } | null
|
|
202
249
|
|
|
203
250
|
if (!existing) {
|
|
204
251
|
res.status(500)
|
|
@@ -247,23 +294,22 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
247
294
|
})
|
|
248
295
|
|
|
249
296
|
if (!authUser) {
|
|
250
|
-
|
|
297
|
+
res.status(401)
|
|
298
|
+
return invalidPasswordError
|
|
251
299
|
}
|
|
252
300
|
|
|
253
|
-
const passwordMatches = await comparePassword(
|
|
254
|
-
req.body.password,
|
|
255
|
-
authUser.password
|
|
256
|
-
)
|
|
301
|
+
const passwordMatches = await comparePassword(req.body.password, authUser.password)
|
|
257
302
|
|
|
258
303
|
if (!passwordMatches) {
|
|
259
|
-
|
|
304
|
+
res.status(401)
|
|
305
|
+
return invalidPasswordError
|
|
260
306
|
}
|
|
261
307
|
|
|
262
308
|
const user =
|
|
263
309
|
user_id_field && userCollection
|
|
264
310
|
? await customUserDb
|
|
265
|
-
|
|
266
|
-
|
|
311
|
+
.collection(userCollection)
|
|
312
|
+
.findOne({ [user_id_field]: authUser._id.toString() })
|
|
267
313
|
: {}
|
|
268
314
|
delete authUser?.password
|
|
269
315
|
|
|
@@ -283,15 +329,19 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
283
329
|
throw new Error(AUTH_ERRORS.USER_NOT_CONFIRMED)
|
|
284
330
|
}
|
|
285
331
|
|
|
332
|
+
const now = new Date()
|
|
286
333
|
const refreshToken = this.createRefreshToken(userWithCustomData)
|
|
287
334
|
const refreshTokenHash = hashToken(refreshToken)
|
|
288
335
|
await authDb.collection(refreshTokensCollection).insertOne({
|
|
289
336
|
userId: authUser._id,
|
|
290
337
|
tokenHash: refreshTokenHash,
|
|
291
|
-
createdAt:
|
|
338
|
+
createdAt: now,
|
|
292
339
|
expiresAt: new Date(Date.now() + refreshTokenTtlMs),
|
|
293
340
|
revokedAt: null
|
|
294
341
|
})
|
|
342
|
+
await authDb
|
|
343
|
+
.collection(authCollection!)
|
|
344
|
+
.updateOne({ _id: authUser._id }, { $set: { lastLoginAt: now } })
|
|
295
345
|
|
|
296
346
|
return {
|
|
297
347
|
access_token: this.createAccessToken(userWithCustomData),
|
|
@@ -347,15 +397,13 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
347
397
|
res.status(429)
|
|
348
398
|
return { message: 'Too many requests' }
|
|
349
399
|
}
|
|
350
|
-
await handleResetPasswordRequest(
|
|
400
|
+
const result = await handleResetPasswordRequest(
|
|
351
401
|
req.body.email,
|
|
352
402
|
req.body.password,
|
|
353
403
|
req.body.arguments
|
|
354
404
|
)
|
|
355
405
|
res.status(202)
|
|
356
|
-
return
|
|
357
|
-
status: 'ok'
|
|
358
|
-
}
|
|
406
|
+
return result
|
|
359
407
|
}
|
|
360
408
|
)
|
|
361
409
|
|
|
@@ -392,12 +440,15 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
392
440
|
}
|
|
393
441
|
|
|
394
442
|
const createdAt = resetRequest.createdAt ? new Date(resetRequest.createdAt) : null
|
|
395
|
-
const isExpired =
|
|
443
|
+
const isExpired =
|
|
444
|
+
!createdAt ||
|
|
396
445
|
Number.isNaN(createdAt.getTime()) ||
|
|
397
446
|
Date.now() - createdAt.getTime() > resetPasswordTtlSeconds * 1000
|
|
398
447
|
|
|
399
448
|
if (isExpired) {
|
|
400
|
-
await authDb
|
|
449
|
+
await authDb
|
|
450
|
+
?.collection(resetPasswordCollection)
|
|
451
|
+
.deleteOne({ _id: resetRequest._id })
|
|
401
452
|
throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
|
|
402
453
|
}
|
|
403
454
|
const hashedPassword = await hashPassword(password)
|
|
@@ -410,7 +461,9 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
410
461
|
}
|
|
411
462
|
)
|
|
412
463
|
|
|
413
|
-
await authDb
|
|
464
|
+
await authDb
|
|
465
|
+
?.collection(resetPasswordCollection)
|
|
466
|
+
.deleteOne({ _id: resetRequest._id })
|
|
414
467
|
}
|
|
415
468
|
)
|
|
416
469
|
}
|
|
@@ -19,13 +19,18 @@ export type ErrorResponseDto = {
|
|
|
19
19
|
message: string
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export type InvalidPasswordResponseDto = {
|
|
23
|
+
error: 'unauthorized'
|
|
24
|
+
error_code: 'InvalidPassword'
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
export interface RegistrationDto {
|
|
23
28
|
Body: RegisterUserDto
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export interface LoginDto {
|
|
27
32
|
Body: LoginUserDto
|
|
28
|
-
Reply: LoginSuccessDto | ErrorResponseDto
|
|
33
|
+
Reply: LoginSuccessDto | ErrorResponseDto | InvalidPasswordResponseDto
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export interface ResetPasswordSendDto {
|
package/src/auth/utils.ts
CHANGED
|
@@ -117,6 +117,7 @@ export enum AUTH_ERRORS {
|
|
|
117
117
|
INVALID_TOKEN = 'Invalid refresh token provided',
|
|
118
118
|
INVALID_RESET_PARAMS = 'Invalid token or tokenId provided',
|
|
119
119
|
MISSING_RESET_FUNCTION = 'Missing reset function',
|
|
120
|
+
INVALID_RESET_FUNCTION_RESPONSE = 'Invalid reset function response',
|
|
120
121
|
USER_NOT_CONFIRMED = 'User not confirmed'
|
|
121
122
|
}
|
|
122
123
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { GenerateContext } from '../../../utils/context'
|
|
2
|
+
import { generateHandler } from '../utils'
|
|
3
|
+
|
|
4
|
+
jest.mock('../../../utils/context', () => ({
|
|
5
|
+
GenerateContext: jest.fn()
|
|
6
|
+
}))
|
|
7
|
+
|
|
8
|
+
const mockedGenerateContext = jest.mocked(GenerateContext)
|
|
9
|
+
|
|
10
|
+
describe('generateHandler', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockedGenerateContext.mockReset()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('allows endpoint functions to set custom response headers', async () => {
|
|
16
|
+
mockedGenerateContext.mockImplementation(async ({ args }) => {
|
|
17
|
+
const [, response] = args as [
|
|
18
|
+
{ body: { text: () => string; rawBody: Buffer | string | undefined } },
|
|
19
|
+
{
|
|
20
|
+
setStatusCode: (code: number) => void
|
|
21
|
+
setHeader: (name: string, value: string | number | readonly string[]) => void
|
|
22
|
+
setBody: (body: unknown) => void
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
response.setStatusCode(201)
|
|
27
|
+
response.setHeader('Content-Type', 'application/json')
|
|
28
|
+
response.setHeader('Cache-Control', 'no-store')
|
|
29
|
+
response.setBody(JSON.stringify({ ok: true }))
|
|
30
|
+
|
|
31
|
+
return { ignored: true }
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const handler = generateHandler({
|
|
35
|
+
app: {} as any,
|
|
36
|
+
currentFunction: { code: 'module.exports = function () {}' } as any,
|
|
37
|
+
functionName: 'endpointHandler',
|
|
38
|
+
functionsList: {
|
|
39
|
+
endpointHandler: { code: 'module.exports = function () {}' }
|
|
40
|
+
} as any,
|
|
41
|
+
http_method: 'POST',
|
|
42
|
+
rulesList: {} as any
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const res = {
|
|
46
|
+
status: jest.fn(),
|
|
47
|
+
header: jest.fn(),
|
|
48
|
+
send: jest.fn((body) => body)
|
|
49
|
+
} as any
|
|
50
|
+
|
|
51
|
+
const response = await handler({
|
|
52
|
+
body: { hello: 'world' },
|
|
53
|
+
headers: { accept: 'application/json' },
|
|
54
|
+
query: { page: '1' },
|
|
55
|
+
rawBody: '{"hello":"world"}',
|
|
56
|
+
user: { id: 'user-1' }
|
|
57
|
+
} as any, res)
|
|
58
|
+
|
|
59
|
+
expect(res.status).toHaveBeenCalledWith(201)
|
|
60
|
+
expect(res.header).toHaveBeenCalledWith('Content-Type', 'application/json')
|
|
61
|
+
expect(res.header).toHaveBeenCalledWith('Cache-Control', 'no-store')
|
|
62
|
+
expect(res.send).toHaveBeenCalledWith(JSON.stringify({ ok: true }))
|
|
63
|
+
expect(response).toBe(JSON.stringify({ ok: true }))
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -138,6 +138,9 @@ export const generateHandler = ({
|
|
|
138
138
|
setStatusCode: (code: number) => {
|
|
139
139
|
res.status(code)
|
|
140
140
|
},
|
|
141
|
+
setHeader: (name: string, value: string | number | readonly string[]) => {
|
|
142
|
+
res.header(name, value)
|
|
143
|
+
},
|
|
141
144
|
setBody: (body: unknown) => {
|
|
142
145
|
customResponseBody.data = body
|
|
143
146
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
mapWatchFilterToChangeStreamMatch,
|
|
4
|
+
mapWatchFilterToDocumentQuery,
|
|
5
|
+
shouldSkipReadabilityLookupForChange
|
|
6
|
+
} from '../controller'
|
|
3
7
|
|
|
4
8
|
describe('watch filter mapping', () => {
|
|
5
9
|
it('keeps change-event fields untouched and prefixes only document fields', () => {
|
|
@@ -113,4 +117,10 @@ describe('watch filter mapping', () => {
|
|
|
113
117
|
expect(documentQuery._id).toEqual(id)
|
|
114
118
|
expect(documentQuery.operationType).toBeUndefined()
|
|
115
119
|
})
|
|
120
|
+
|
|
121
|
+
it('skips readability lookup only for delete change events', () => {
|
|
122
|
+
expect(shouldSkipReadabilityLookupForChange({ operationType: 'delete' } as any)).toBe(true)
|
|
123
|
+
expect(shouldSkipReadabilityLookupForChange({ operationType: 'update' } as any)).toBe(false)
|
|
124
|
+
expect(shouldSkipReadabilityLookupForChange({ operationType: 'insert' } as any)).toBe(false)
|
|
125
|
+
})
|
|
116
126
|
})
|
|
@@ -315,6 +315,9 @@ const isReadableDocumentResult = (value: unknown) =>
|
|
|
315
315
|
!Array.isArray(value) &&
|
|
316
316
|
Object.keys(value as Record<string, unknown>).length > 0
|
|
317
317
|
|
|
318
|
+
export const shouldSkipReadabilityLookupForChange = (change: Document) =>
|
|
319
|
+
change.operationType === 'delete'
|
|
320
|
+
|
|
318
321
|
/**
|
|
319
322
|
* > Creates a pre handler for every query
|
|
320
323
|
* @param app -> the fastify instance
|
|
@@ -524,6 +527,11 @@ export const functionsController: FunctionController = async (
|
|
|
524
527
|
(change as { fullDocument?: { _id?: unknown } })?.fullDocument?._id
|
|
525
528
|
if (typeof docId === 'undefined') return
|
|
526
529
|
|
|
530
|
+
if (shouldSkipReadabilityLookupForChange(change)) {
|
|
531
|
+
subscriberRes.write(`data: ${serializeEjson(change)}\n\n`)
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
527
535
|
const readQuery = subscriber.documentFilter
|
|
528
536
|
? ({ $and: [subscriber.documentFilter, { _id: docId }] } as Document)
|
|
529
537
|
: ({ _id: docId } as Document)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Document } from 'mongodb'
|
|
2
|
+
export type PermissionExpression = boolean | Record<string, unknown>
|
|
3
|
+
|
|
2
4
|
export interface Filter {
|
|
3
5
|
name: string
|
|
4
6
|
query: Record<string, unknown>
|
|
@@ -9,11 +11,11 @@ export type Projection = Record<string, 0 | 1>
|
|
|
9
11
|
export interface Role {
|
|
10
12
|
name: string
|
|
11
13
|
apply_when: Record<string, unknown>
|
|
12
|
-
insert:
|
|
13
|
-
delete:
|
|
14
|
-
search:
|
|
15
|
-
read:
|
|
16
|
-
write:
|
|
14
|
+
insert: PermissionExpression
|
|
15
|
+
delete: PermissionExpression
|
|
16
|
+
search: PermissionExpression
|
|
17
|
+
read: PermissionExpression
|
|
18
|
+
write: PermissionExpression
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface RulesConfig {
|
|
@@ -21,7 +23,6 @@ export interface RulesConfig {
|
|
|
21
23
|
collection: string
|
|
22
24
|
filters: Filter[]
|
|
23
25
|
roles: Role[]
|
|
24
|
-
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export type Rules = Record<string, RulesConfig>
|
|
@@ -38,21 +39,21 @@ export type AggregationPipelineStage =
|
|
|
38
39
|
| { $unionWith: UnionWithStage }
|
|
39
40
|
|
|
40
41
|
export interface LookupStage {
|
|
41
|
-
from: string
|
|
42
|
-
localField?: string
|
|
43
|
-
foreignField?: string
|
|
44
|
-
as: string
|
|
45
|
-
let?: Record<string, unknown
|
|
46
|
-
pipeline?: AggregationPipelineStage[]
|
|
42
|
+
from: string
|
|
43
|
+
localField?: string
|
|
44
|
+
foreignField?: string
|
|
45
|
+
as: string
|
|
46
|
+
let?: Record<string, unknown>
|
|
47
|
+
pipeline?: AggregationPipelineStage[]
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export type AggregationPipeline = Document[]
|
|
50
51
|
|
|
51
52
|
export type UnionWithStage = string | UnionWithNestedStage
|
|
52
|
-
type UnionWithNestedStage = { coll: string
|
|
53
|
+
type UnionWithNestedStage = { coll: string; pipeline: AggregationPipelineStage[] }
|
|
53
54
|
|
|
54
55
|
export enum STAGES_TO_SEARCH {
|
|
55
|
-
LOOKUP =
|
|
56
|
-
UNION_WITH =
|
|
57
|
-
FACET =
|
|
58
|
-
}
|
|
56
|
+
LOOKUP = '$lookup',
|
|
57
|
+
UNION_WITH = '$unionWith',
|
|
58
|
+
FACET = '$facet'
|
|
59
|
+
}
|