@volcanicminds/backend 2.3.1 → 3.0.0
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 +240 -28
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -36
- package/dist/index.js.map +1 -1
- package/dist/lib/api/auth/controller/auth.d.ts.map +1 -1
- package/dist/lib/api/auth/controller/auth.js +14 -7
- package/dist/lib/api/auth/controller/auth.js.map +1 -1
- package/dist/lib/api/health/controller/health.d.ts +1 -1
- package/dist/lib/api/health/controller/health.d.ts.map +1 -1
- package/dist/lib/api/health/controller/health.js +1 -1
- package/dist/lib/api/health/controller/health.js.map +1 -1
- package/dist/lib/api/health/routes.d.ts +1 -0
- package/dist/lib/api/health/routes.d.ts.map +1 -1
- package/dist/lib/api/health/routes.js +1 -0
- package/dist/lib/api/health/routes.js.map +1 -1
- package/dist/lib/api/tenants/controller/tenants.d.ts +17 -0
- package/dist/lib/api/tenants/controller/tenants.d.ts.map +1 -0
- package/dist/lib/api/tenants/controller/tenants.js +150 -0
- package/dist/lib/api/tenants/controller/tenants.js.map +1 -0
- package/dist/lib/api/tenants/routes.d.ts +167 -0
- package/dist/lib/api/tenants/routes.d.ts.map +1 -0
- package/dist/lib/api/tenants/routes.js +143 -0
- package/dist/lib/api/tenants/routes.js.map +1 -0
- package/dist/lib/config/plugins.js +1 -1
- package/dist/lib/config/plugins.js.map +1 -1
- package/dist/lib/database/typeorm/entities/change.d.ts +11 -0
- package/dist/lib/database/typeorm/entities/change.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/change.js +4 -0
- package/dist/lib/database/typeorm/entities/change.js.map +1 -0
- package/dist/lib/database/typeorm/entities/tenant.d.ts +13 -0
- package/dist/lib/database/typeorm/entities/tenant.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/tenant.js +4 -0
- package/dist/lib/database/typeorm/entities/tenant.js.map +1 -0
- package/dist/lib/database/typeorm/entities/token.d.ts +16 -0
- package/dist/lib/database/typeorm/entities/token.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/token.js +4 -0
- package/dist/lib/database/typeorm/entities/token.js.map +1 -0
- package/dist/lib/database/typeorm/entities/user.d.ts +28 -0
- package/dist/lib/database/typeorm/entities/user.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/user.js +4 -0
- package/dist/lib/database/typeorm/entities/user.js.map +1 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.d.ts +6 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.js +73 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.js.map +1 -0
- package/dist/lib/database/typeorm/loader/entities.d.ts +6 -0
- package/dist/lib/database/typeorm/loader/entities.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/entities.js +37 -0
- package/dist/lib/database/typeorm/loader/entities.js.map +1 -0
- package/dist/lib/database/typeorm/loader/tenantManager.d.ts +17 -0
- package/dist/lib/database/typeorm/loader/tenantManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/tenantManager.js +193 -0
- package/dist/lib/database/typeorm/loader/tenantManager.js.map +1 -0
- package/dist/lib/database/typeorm/loader/tokenManager.d.ts +23 -0
- package/dist/lib/database/typeorm/loader/tokenManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/tokenManager.js +131 -0
- package/dist/lib/database/typeorm/loader/tokenManager.js.map +1 -0
- package/dist/lib/database/typeorm/loader/userManager.d.ts +39 -0
- package/dist/lib/database/typeorm/loader/userManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/userManager.js +358 -0
- package/dist/lib/database/typeorm/loader/userManager.js.map +1 -0
- package/dist/lib/database/typeorm/query/builder.d.ts +2 -0
- package/dist/lib/database/typeorm/query/builder.d.ts.map +1 -0
- package/dist/lib/database/typeorm/query/builder.js +33 -0
- package/dist/lib/database/typeorm/query/builder.js.map +1 -0
- package/dist/lib/database/typeorm/query/parser.d.ts +2 -0
- package/dist/lib/database/typeorm/query/parser.d.ts.map +1 -0
- package/dist/lib/database/typeorm/query/parser.js +27 -0
- package/dist/lib/database/typeorm/query/parser.js.map +1 -0
- package/dist/lib/database/typeorm/query.d.ts +36 -0
- package/dist/lib/database/typeorm/query.d.ts.map +1 -0
- package/dist/lib/database/typeorm/query.js +259 -0
- package/dist/lib/database/typeorm/query.js.map +1 -0
- package/dist/lib/database/typeorm/util/crypto.d.ts +3 -0
- package/dist/lib/database/typeorm/util/crypto.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/crypto.js +46 -0
- package/dist/lib/database/typeorm/util/crypto.js.map +1 -0
- package/dist/lib/database/typeorm/util/error.d.ts +5 -0
- package/dist/lib/database/typeorm/util/error.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/error.js +8 -0
- package/dist/lib/database/typeorm/util/error.js.map +1 -0
- package/dist/lib/database/typeorm/util/logger.d.ts +7 -0
- package/dist/lib/database/typeorm/util/logger.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/logger.js +31 -0
- package/dist/lib/database/typeorm/util/logger.js.map +1 -0
- package/dist/lib/database/typeorm/util/yn.d.ts +2 -0
- package/dist/lib/database/typeorm/util/yn.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/yn.js +15 -0
- package/dist/lib/database/typeorm/util/yn.js.map +1 -0
- package/dist/lib/defaults/managers.d.ts.map +1 -1
- package/dist/lib/defaults/managers.js +186 -58
- package/dist/lib/defaults/managers.js.map +1 -1
- package/dist/lib/hooks/onRequest.d.ts.map +1 -1
- package/dist/lib/hooks/onRequest.js +11 -48
- package/dist/lib/hooks/onRequest.js.map +1 -1
- package/dist/lib/loader/router.d.ts.map +1 -1
- package/dist/lib/loader/router.js +92 -83
- package/dist/lib/loader/router.js.map +1 -1
- package/dist/lib/loader/tenant.d.ts.map +1 -1
- package/dist/lib/loader/tenant.js +25 -2
- package/dist/lib/loader/tenant.js.map +1 -1
- package/dist/lib/locales/en.json +12 -0
- package/dist/lib/locales/it.json +12 -0
- package/dist/lib/schemas/tenant.d.ts +88 -0
- package/dist/lib/schemas/tenant.d.ts.map +1 -0
- package/dist/lib/schemas/tenant.js +45 -0
- package/dist/lib/schemas/tenant.js.map +1 -0
- package/dist/lib/util/regexp.d.ts +2 -0
- package/dist/lib/util/regexp.d.ts.map +1 -1
- package/dist/lib/util/regexp.js +5 -3
- package/dist/lib/util/regexp.js.map +1 -1
- package/dist/lib/util/secret.d.ts +11 -0
- package/dist/lib/util/secret.d.ts.map +1 -0
- package/dist/lib/util/secret.js +70 -0
- package/dist/lib/util/secret.js.map +1 -0
- package/dist/server.js +4 -1
- package/dist/server.js.map +1 -1
- package/dist/typeorm.d.ts +15 -0
- package/dist/typeorm.d.ts.map +1 -0
- package/dist/typeorm.js +86 -0
- package/dist/typeorm.js.map +1 -0
- package/dist/types/database/typeorm/global.d.ts +56 -0
- package/dist/types/database/typeorm/global.d.ts.map +1 -0
- package/dist/types/database/typeorm/global.js +2 -0
- package/dist/types/database/typeorm/global.js.map +1 -0
- package/dist/types/global.d.ts +318 -0
- package/dist/types/orm.d.ts +22 -0
- package/lib/api/auth/controller/auth.ts +28 -12
- package/lib/api/health/controller/health.ts +1 -1
- package/lib/api/health/routes.ts +1 -0
- package/lib/api/tenants/controller/tenants.ts +211 -0
- package/lib/api/tenants/routes.ts +145 -0
- package/lib/config/plugins.ts +1 -1
- package/lib/database/typeorm/entities/change.ts +11 -0
- package/lib/database/typeorm/entities/tenant.ts +20 -0
- package/lib/database/typeorm/entities/token.ts +18 -0
- package/lib/database/typeorm/entities/user.ts +33 -0
- package/lib/database/typeorm/loader/dataBaseManager.ts +89 -0
- package/lib/database/typeorm/loader/entities.ts +47 -0
- package/lib/database/typeorm/loader/tenantManager.ts +260 -0
- package/lib/database/typeorm/loader/tokenManager.ts +157 -0
- package/lib/database/typeorm/loader/userManager.ts +432 -0
- package/lib/database/typeorm/query/builder.ts +39 -0
- package/lib/database/typeorm/query/parser.ts +29 -0
- package/lib/database/typeorm/query.ts +326 -0
- package/lib/database/typeorm/util/crypto.ts +52 -0
- package/lib/database/typeorm/util/error.ts +7 -0
- package/lib/database/typeorm/util/logger.ts +35 -0
- package/lib/database/typeorm/util/yn.ts +20 -0
- package/lib/defaults/managers.ts +191 -64
- package/lib/hooks/onRequest.ts +12 -64
- package/lib/loader/router.ts +133 -110
- package/lib/loader/tenant.ts +37 -14
- package/lib/schemas/tenant.ts +49 -0
- package/lib/util/regexp.ts +31 -8
- package/lib/util/secret.ts +113 -0
- package/package.json +54 -13
- package/dist/lib/apollo/context.d.ts +0 -6
- package/dist/lib/apollo/context.d.ts.map +0 -1
- package/dist/lib/apollo/context.js +0 -5
- package/dist/lib/apollo/context.js.map +0 -1
- package/dist/lib/apollo/resolvers.d.ts +0 -8
- package/dist/lib/apollo/resolvers.d.ts.map +0 -1
- package/dist/lib/apollo/resolvers.js +0 -8
- package/dist/lib/apollo/resolvers.js.map +0 -1
- package/dist/lib/apollo/type-defs.d.ts +0 -3
- package/dist/lib/apollo/type-defs.d.ts.map +0 -1
- package/dist/lib/apollo/type-defs.js +0 -8
- package/dist/lib/apollo/type-defs.js.map +0 -1
- package/lib/apollo/context.ts +0 -11
- package/lib/apollo/resolvers.ts +0 -12
- package/lib/apollo/type-defs.ts +0 -9
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/* eslint-disable no-useless-catch */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import * as Crypto from 'crypto'
|
|
4
|
+
import { QueryRunner, EntityManager } from 'typeorm'
|
|
5
|
+
import { ServiceError } from '../util/error.js'
|
|
6
|
+
import { executeCountQuery, executeFindQuery } from '../query.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the appropriate token repository.
|
|
10
|
+
* Use this to support both explicit EntityManager (req.db) and QueryRunner.
|
|
11
|
+
*/
|
|
12
|
+
function getTokenRepo(context?: QueryRunner | EntityManager) {
|
|
13
|
+
const { multi_tenant } = (global as any).config?.options || {}
|
|
14
|
+
|
|
15
|
+
if (multi_tenant?.enabled && !context) {
|
|
16
|
+
throw new Error('[TokenManager] ⛔️ CRITICAL: Missing DB Context (Runner/Manager) in Multi-Tenant Environment!')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// If no context provided (and not multi-tenant), fallback to global
|
|
20
|
+
if (!context) {
|
|
21
|
+
return global.connection.getRepository(global.entity.Token)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if it's a QueryRunner (has .manager property)
|
|
25
|
+
if ((context as QueryRunner).manager) {
|
|
26
|
+
return (context as QueryRunner).manager.getRepository(global.entity.Token)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Otherwise treat as EntityManager
|
|
30
|
+
return (context as EntityManager).getRepository(global.entity.Token)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isImplemented() {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function isValidToken(data: typeof global.entity.Token) {
|
|
38
|
+
return !!data && (!!data._id || !!data.id) && !!data.externalId && !!data.name
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createToken(data: typeof global.entity.Token, runner?: QueryRunner) {
|
|
42
|
+
const { name, description } = data
|
|
43
|
+
|
|
44
|
+
if (!name) {
|
|
45
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const repo = getTokenRepo(runner)
|
|
50
|
+
let externalId, token
|
|
51
|
+
do {
|
|
52
|
+
externalId = Crypto.randomUUID({ disableEntropyCache: true })
|
|
53
|
+
token = await repo.findOneBy({ externalId: externalId })
|
|
54
|
+
} while (token != null)
|
|
55
|
+
|
|
56
|
+
const newToken = repo.create({
|
|
57
|
+
...data,
|
|
58
|
+
name: name,
|
|
59
|
+
description: description,
|
|
60
|
+
blocked: false,
|
|
61
|
+
blockedReason: null,
|
|
62
|
+
externalId: externalId
|
|
63
|
+
} as typeof global.entity.Token)
|
|
64
|
+
|
|
65
|
+
return repo.save(newToken)
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function resetExternalId(id: string, runner?: QueryRunner) {
|
|
72
|
+
if (!id) {
|
|
73
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const repo = getTokenRepo(runner)
|
|
78
|
+
let externalId, token
|
|
79
|
+
do {
|
|
80
|
+
externalId = Crypto.randomUUID({ disableEntropyCache: true })
|
|
81
|
+
token = await repo.findOneBy({ externalId: externalId })
|
|
82
|
+
} while (token != null)
|
|
83
|
+
|
|
84
|
+
return await updateTokenById(id, { externalId: externalId }, runner)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error?.code == 23505) {
|
|
87
|
+
throw new ServiceError('External ID not changed', 409)
|
|
88
|
+
}
|
|
89
|
+
throw error
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function updateTokenById(id: string, token: typeof global.entity.Token, runner?: QueryRunner) {
|
|
94
|
+
if (!id || !token) {
|
|
95
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const repo = getTokenRepo(runner)
|
|
99
|
+
const tokenEx = await repo.findOneBy({ id: id })
|
|
100
|
+
if (!tokenEx) {
|
|
101
|
+
throw new ServiceError('Token not found', 404)
|
|
102
|
+
}
|
|
103
|
+
const merged = repo.merge(tokenEx, token)
|
|
104
|
+
return repo.save(merged)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function retrieveTokenById(id: string, runner?: QueryRunner) {
|
|
111
|
+
if (!id) {
|
|
112
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
return await getTokenRepo(runner).findOneBy({ id: id })
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw error
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function retrieveTokenByExternalId(externalId: string, runner?: QueryRunner) {
|
|
122
|
+
if (!externalId) {
|
|
123
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return await getTokenRepo(runner).findOneBy({ externalId: externalId })
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw error
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function blockTokenById(id: string, reason: string, runner?: QueryRunner) {
|
|
133
|
+
return updateTokenById(id, { blocked: true, blockedAt: new Date(), blockedReason: reason }, runner)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function unblockTokenById(id: string, runner?: QueryRunner) {
|
|
137
|
+
return updateTokenById(id, { blocked: false, blockedAt: new Date(), blockedReason: null }, runner)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function countQuery(data: any, runner?: QueryRunner) {
|
|
141
|
+
return await executeCountQuery(getTokenRepo(runner), data, {})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function findQuery(data: any, runner?: QueryRunner) {
|
|
145
|
+
return await executeFindQuery(getTokenRepo(runner), {}, data)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function removeTokenById(id: string, runner?: QueryRunner) {
|
|
149
|
+
if (!id) {
|
|
150
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return await getTokenRepo(runner).delete(id)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw error
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/* eslint-disable no-useless-catch */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import * as bcrypt from 'bcrypt'
|
|
4
|
+
import * as Crypto from 'crypto'
|
|
5
|
+
import { QueryRunner, EntityManager } from 'typeorm'
|
|
6
|
+
import { ServiceError } from '../util/error.js'
|
|
7
|
+
import { executeCountQuery, executeFindQuery } from '../query.js'
|
|
8
|
+
import { encrypt, decrypt } from '../util/crypto.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns the appropriate user repository.
|
|
12
|
+
* Use this to support both explicit EntityManager (req.db) and QueryRunner.
|
|
13
|
+
*/
|
|
14
|
+
function getUserRepo(context?: QueryRunner | EntityManager) {
|
|
15
|
+
const { multi_tenant } = (global as any).config?.options || {}
|
|
16
|
+
|
|
17
|
+
// Hardening: In Multi-Tenant, we MUST have a context (Runner or Manager).
|
|
18
|
+
// Implicit fallback to global.connection is dangerous.
|
|
19
|
+
if (multi_tenant?.enabled && !context) {
|
|
20
|
+
const errorMsg =
|
|
21
|
+
'[UserManager] ⛔️ CRITICAL: Missing DB Context (Runner/Manager) in Multi-Tenant Environment! Implicit fallback to public schema forbidden.'
|
|
22
|
+
console.error(errorMsg)
|
|
23
|
+
throw new Error(errorMsg)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If no context provided (and not multi-tenant), fallback to global
|
|
27
|
+
if (!context) {
|
|
28
|
+
return global.connection.getRepository(global.entity.User)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if it's a QueryRunner (has .manager property)
|
|
32
|
+
if ((context as QueryRunner).manager) {
|
|
33
|
+
return (context as QueryRunner).manager.getRepository(global.entity.User)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Otherwise treat as EntityManager
|
|
37
|
+
return (context as EntityManager).getRepository(global.entity.User)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isImplemented() {
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function isValidUser(data: typeof global.entity.User) {
|
|
45
|
+
return !!data && (!!data._id || !!data.id) && !!data.externalId && !!data.email && !!data.password
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function createUser(data: typeof global.entity.User, runner?: QueryRunner) {
|
|
49
|
+
const { username, email, password } = data
|
|
50
|
+
|
|
51
|
+
if (!email || !password) {
|
|
52
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const salt = await bcrypt.genSalt(12)
|
|
56
|
+
const hashedPassword = await bcrypt.hash(password, salt)
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const repo = getUserRepo(runner)
|
|
60
|
+
let externalId, user
|
|
61
|
+
do {
|
|
62
|
+
externalId = Crypto.randomUUID({ disableEntropyCache: true })
|
|
63
|
+
|
|
64
|
+
user = await repo.findOneBy({ externalId: externalId })
|
|
65
|
+
} while (user != null)
|
|
66
|
+
|
|
67
|
+
const newUser = repo.create({
|
|
68
|
+
...data,
|
|
69
|
+
passwordChangedAt: new Date(),
|
|
70
|
+
confirmed: false,
|
|
71
|
+
confirmationToken: Crypto.randomBytes(64).toString('hex'),
|
|
72
|
+
blocked: false,
|
|
73
|
+
blockedReason: null,
|
|
74
|
+
externalId: externalId,
|
|
75
|
+
email: email,
|
|
76
|
+
username: username || email,
|
|
77
|
+
password: hashedPassword,
|
|
78
|
+
mfaEnabled: false,
|
|
79
|
+
mfaSecret: null,
|
|
80
|
+
mfaType: 'TOTP',
|
|
81
|
+
mfaRecoveryCodes: []
|
|
82
|
+
} as typeof global.entity.User)
|
|
83
|
+
|
|
84
|
+
return getUserRepo(runner).save(newUser)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error?.code == 23505) {
|
|
87
|
+
throw new ServiceError('Email or username already registered', 409)
|
|
88
|
+
}
|
|
89
|
+
throw error
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function deleteUser(id: string, runner?: QueryRunner) {
|
|
94
|
+
if (!id) {
|
|
95
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const userEx = await retrieveUserById(id, runner)
|
|
100
|
+
if (!userEx) {
|
|
101
|
+
throw new ServiceError('User not found', 404)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return getUserRepo(runner).delete(id)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function resetExternalId(id: string, runner?: QueryRunner) {
|
|
111
|
+
if (!id) {
|
|
112
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
let externalId, user
|
|
117
|
+
do {
|
|
118
|
+
externalId = Crypto.randomUUID({ disableEntropyCache: true })
|
|
119
|
+
user = await getUserRepo(runner).findOneBy({ externalId: externalId })
|
|
120
|
+
} while (user != null)
|
|
121
|
+
|
|
122
|
+
return await updateUserById(id, { externalId: externalId }, runner)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code == 23505) {
|
|
125
|
+
throw new ServiceError('External ID not changed', 409)
|
|
126
|
+
}
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function updateUserById(id: string, user: typeof global.entity.User, runner?: QueryRunner) {
|
|
132
|
+
if (!id || !user) {
|
|
133
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const repo = getUserRepo(runner)
|
|
137
|
+
const userEx = await retrieveUserById(id, runner)
|
|
138
|
+
if (!userEx) {
|
|
139
|
+
throw new ServiceError('User not found', 404)
|
|
140
|
+
}
|
|
141
|
+
const merged = repo.merge(userEx, user)
|
|
142
|
+
return repo.save(merged)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw error
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function retrieveUserById(id: string, runner?: QueryRunner) {
|
|
149
|
+
if (!id) {
|
|
150
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return await getUserRepo(runner).findOneBy({ id: id })
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw error
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function retrieveUserByEmail(email: string, runner?: QueryRunner) {
|
|
160
|
+
if (!email) {
|
|
161
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
return await getUserRepo(runner).findOneBy({ email: email })
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw error
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function retrieveUserByUsername(username: string, runner?: QueryRunner) {
|
|
171
|
+
if (!username) {
|
|
172
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
return await getUserRepo(runner).findOneBy({ username })
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw error
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function retrieveUserByConfirmationToken(code: string, runner?: QueryRunner) {
|
|
182
|
+
if (!code) {
|
|
183
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
return await getUserRepo(runner).findOneBy({ confirmationToken: code })
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function retrieveUserByResetPasswordToken(code: string, runner?: QueryRunner) {
|
|
193
|
+
if (!code) {
|
|
194
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
return await getUserRepo(runner).findOneBy({ resetPasswordToken: code })
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw error
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function retrieveUserByExternalId(externalId: string, runner?: QueryRunner) {
|
|
204
|
+
if (!externalId) {
|
|
205
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
return await getUserRepo(runner).findOne({
|
|
209
|
+
where: { externalId: externalId },
|
|
210
|
+
cache: global.cacheTimeout
|
|
211
|
+
})
|
|
212
|
+
} catch (error) {
|
|
213
|
+
throw error
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Constant-cost bcrypt hash (cost 12, matching createUser) compared against when
|
|
218
|
+
// the user does not exist, so response time is the same whether or not the email
|
|
219
|
+
// is registered. Prevents user enumeration via timing. The plaintext is irrelevant.
|
|
220
|
+
const DUMMY_PASSWORD_HASH = '$2b$12$4sLKI6Ag4n6KjUBPqA4oJuAthEdYgbwUj7oIR8yj7IekjUCzUFRD2'
|
|
221
|
+
|
|
222
|
+
export async function retrieveUserByPassword(email: string, password: string, runner?: QueryRunner) {
|
|
223
|
+
if (!email || !password) {
|
|
224
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
225
|
+
}
|
|
226
|
+
const user = await getUserRepo(runner).findOneBy({ email: email })
|
|
227
|
+
// Always run a bcrypt comparison (against a dummy hash when the user is missing)
|
|
228
|
+
// to keep the timing constant and avoid leaking whether the email exists.
|
|
229
|
+
const match = await bcrypt.compare(password, user?.password || DUMMY_PASSWORD_HASH)
|
|
230
|
+
return user && match ? user : null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function changePassword(email: string, password: string, oldPassword: string, runner?: QueryRunner) {
|
|
234
|
+
if (!email || !password || !oldPassword) {
|
|
235
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const repo = getUserRepo(runner)
|
|
239
|
+
const user = await repo.findOneBy({ email: email })
|
|
240
|
+
// Guard the null deref (user not found) and keep the bcrypt cost constant
|
|
241
|
+
// regardless of existence, mirroring retrieveUserByPassword.
|
|
242
|
+
const match = await bcrypt.compare(oldPassword, user?.password || DUMMY_PASSWORD_HASH)
|
|
243
|
+
if (user && match) {
|
|
244
|
+
const salt = await bcrypt.genSalt(12)
|
|
245
|
+
const hashedPassword = await bcrypt.hash(password, salt)
|
|
246
|
+
return repo.save({ ...user, passwordChangedAt: new Date(), password: hashedPassword })
|
|
247
|
+
}
|
|
248
|
+
throw new ServiceError('Password not changed', 400)
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw error
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function forgotPassword(email: string, runner?: QueryRunner) {
|
|
255
|
+
if (!email) {
|
|
256
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const repo = getUserRepo(runner)
|
|
260
|
+
const user = await repo.findOneBy({ email: email })
|
|
261
|
+
|
|
262
|
+
if (user) {
|
|
263
|
+
return repo.save({
|
|
264
|
+
...user,
|
|
265
|
+
resetPasswordTokenAt: new Date(),
|
|
266
|
+
resetPasswordToken: Crypto.randomBytes(64).toString('hex')
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
throw new ServiceError('Password not changed', 400)
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw error
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function resetPassword(user: typeof global.entity.User, password: string, runner?: QueryRunner) {
|
|
276
|
+
if (!user || !password || !user?.email) {
|
|
277
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const repo = getUserRepo(runner)
|
|
281
|
+
const userEx = await repo.findOneBy({ email: user.email })
|
|
282
|
+
if (!userEx) {
|
|
283
|
+
throw new Error('Wrong credentials')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const salt = await bcrypt.genSalt(12)
|
|
287
|
+
const hashedPassword = await bcrypt.hash(password, salt)
|
|
288
|
+
return repo.save({
|
|
289
|
+
...userEx,
|
|
290
|
+
passwordChangedAt: new Date(),
|
|
291
|
+
confirmed: true,
|
|
292
|
+
confirmedAt: new Date(),
|
|
293
|
+
resetPasswordToken: null,
|
|
294
|
+
password: hashedPassword
|
|
295
|
+
})
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw error
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function userConfirmation(user: typeof global.entity.User, runner?: QueryRunner) {
|
|
302
|
+
if (!user) {
|
|
303
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
return getUserRepo(runner).save({ ...user, confirmed: true, confirmedAt: new Date(), confirmationToken: null })
|
|
307
|
+
} catch (error) {
|
|
308
|
+
throw error
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function blockUserById(id: string, reason: string, runner?: QueryRunner) {
|
|
313
|
+
return updateUserById(id, { blocked: true, blockedAt: new Date(), blockedReason: reason }, runner)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function unblockUserById(id: string, runner?: QueryRunner) {
|
|
317
|
+
return updateUserById(id, { blocked: false, blockedAt: new Date(), blockedReason: null }, runner)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function isPasswordToBeChanged(user: typeof global.entity.User) {
|
|
321
|
+
if (process.env.PASSWORD_EXPIRATION_DAYS != null) {
|
|
322
|
+
let passwordExpirationDays = -1
|
|
323
|
+
try {
|
|
324
|
+
passwordExpirationDays = Number(process.env.PASSWORD_EXPIRATION_DAYS)
|
|
325
|
+
if (passwordExpirationDays <= 0) {
|
|
326
|
+
throw new Error('PASSWORD_EXPIRATION_DAYS_ENV_INVALID')
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {
|
|
329
|
+
// `e` is already an Error; rethrow it as-is. `new Error(e)` stringified the
|
|
330
|
+
// Error into a useless "[object …]" / doubly-wrapped message.
|
|
331
|
+
throw e
|
|
332
|
+
}
|
|
333
|
+
const { passwordChangedAt } = user
|
|
334
|
+
const date1 = new Date(passwordChangedAt)
|
|
335
|
+
const date2 = new Date()
|
|
336
|
+
const differenceInTime = date2.getTime() - date1.getTime()
|
|
337
|
+
const differenceInDays = differenceInTime / (1000 * 3600 * 24)
|
|
338
|
+
|
|
339
|
+
return differenceInDays >= passwordExpirationDays
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function countQuery(data: any, runner?: QueryRunner) {
|
|
346
|
+
return await executeCountQuery(getUserRepo(runner), data)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function findQuery(data: any, runner?: QueryRunner) {
|
|
350
|
+
return await executeFindQuery(getUserRepo(runner), {}, data)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function disableUserById(id: string, runner?: QueryRunner) {
|
|
354
|
+
await updateUserById(
|
|
355
|
+
id,
|
|
356
|
+
{ blocked: true, blockedAt: new Date(), blockedReason: 'User disabled to unregister' },
|
|
357
|
+
runner
|
|
358
|
+
)
|
|
359
|
+
return resetExternalId(id, runner)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// MFA Persistence Methods
|
|
363
|
+
|
|
364
|
+
export async function saveMfaSecret(userId: string, secret: string, runner?: QueryRunner) {
|
|
365
|
+
if (!userId || !secret) {
|
|
366
|
+
throw new ServiceError('Invalid parameters', 400)
|
|
367
|
+
}
|
|
368
|
+
const encryptedSecret = encrypt(secret)
|
|
369
|
+
|
|
370
|
+
await updateUserById(
|
|
371
|
+
userId,
|
|
372
|
+
{
|
|
373
|
+
mfaSecret: encryptedSecret,
|
|
374
|
+
mfaType: 'TOTP'
|
|
375
|
+
},
|
|
376
|
+
runner
|
|
377
|
+
)
|
|
378
|
+
return true
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function retrieveMfaSecret(userId: string, runner?: QueryRunner) {
|
|
382
|
+
if (!userId) throw new ServiceError('Invalid parameters', 400)
|
|
383
|
+
|
|
384
|
+
const user = await getUserRepo(runner)
|
|
385
|
+
.createQueryBuilder('user')
|
|
386
|
+
.addSelect('user.mfaSecret')
|
|
387
|
+
.where('user.id = :id', { id: userId })
|
|
388
|
+
.getOne()
|
|
389
|
+
|
|
390
|
+
if (!user || !user.mfaSecret) return null
|
|
391
|
+
|
|
392
|
+
return decrypt(user.mfaSecret)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function enableMfa(userId: string, runner?: QueryRunner) {
|
|
396
|
+
if (!userId) throw new ServiceError('Invalid parameters', 400)
|
|
397
|
+
return await updateUserById(userId, { mfaEnabled: true }, runner)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function disableMfa(userId: string, runner?: QueryRunner) {
|
|
401
|
+
if (!userId) throw new ServiceError('Invalid parameters', 400)
|
|
402
|
+
return await updateUserById(userId, { mfaEnabled: false, mfaSecret: null, mfaRecoveryCodes: [] }, runner)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function forceDisableMfaForAdmin(email: string, runner?: QueryRunner) {
|
|
406
|
+
if (!email) return false
|
|
407
|
+
|
|
408
|
+
const user = await getUserRepo(runner).findOneBy({ email: email })
|
|
409
|
+
if (!user) {
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Verify admin role (supports array of strings)
|
|
414
|
+
const hasAdmin =
|
|
415
|
+
user.roles && (user.roles.includes('admin') || user.roles.some((r) => r === 'admin' || r.code === 'admin'))
|
|
416
|
+
|
|
417
|
+
if (!hasAdmin) {
|
|
418
|
+
return false
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await updateUserById(
|
|
422
|
+
user.id,
|
|
423
|
+
{
|
|
424
|
+
mfaEnabled: false,
|
|
425
|
+
mfaSecret: null,
|
|
426
|
+
mfaRecoveryCodes: []
|
|
427
|
+
},
|
|
428
|
+
runner
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return true
|
|
432
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
const combineSqlAnd = (left, right) => {
|
|
3
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
4
|
+
throw new Error('Combining OR conditions with AND is not supported in this simplified SQL builder.')
|
|
5
|
+
}
|
|
6
|
+
return { ...left, ...right }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const buildWhereFromAst = (ast: any, aliasMap: Map<string, any>, isMongo: boolean) => {
|
|
10
|
+
if (!ast) {
|
|
11
|
+
return {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (ast.type === 'operand') {
|
|
15
|
+
if (!aliasMap.has(ast.value)) {
|
|
16
|
+
throw new Error(`Alias "${ast.value}" used in _logic not found in query parameters.`)
|
|
17
|
+
}
|
|
18
|
+
return aliasMap.get(ast.value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const left = buildWhereFromAst(ast.left, aliasMap, isMongo)
|
|
22
|
+
const right = buildWhereFromAst(ast.right, aliasMap, isMongo)
|
|
23
|
+
|
|
24
|
+
if (ast.type === 'AND') {
|
|
25
|
+
if (isMongo) {
|
|
26
|
+
return { $and: [left, right].flat() }
|
|
27
|
+
}
|
|
28
|
+
return combineSqlAnd(left, right)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (ast.type === 'OR') {
|
|
32
|
+
if (isMongo) {
|
|
33
|
+
return { $or: [left, right].flat() }
|
|
34
|
+
}
|
|
35
|
+
return [left, right].flat()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const parseLogicExpression = (logic: string) => {
|
|
2
|
+
const tokens = logic.match(/\(|\)|[A-Za-z0-9_:]+|\s*(AND|OR)\s*/g)?.filter((t) => t.trim()) || []
|
|
3
|
+
let pos = 0
|
|
4
|
+
|
|
5
|
+
const parseExpression = () => {
|
|
6
|
+
let left = parseTerm()
|
|
7
|
+
while (pos < tokens.length && (tokens[pos].trim() === 'OR' || tokens[pos].trim() === 'AND')) {
|
|
8
|
+
const operator = tokens[pos++].trim()
|
|
9
|
+
const right = parseTerm()
|
|
10
|
+
left = { type: operator, left, right }
|
|
11
|
+
}
|
|
12
|
+
return left
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parseTerm = () => {
|
|
16
|
+
if (tokens[pos] === '(') {
|
|
17
|
+
pos++
|
|
18
|
+
const node = parseExpression()
|
|
19
|
+
if (tokens[pos] !== ')') {
|
|
20
|
+
throw new Error('Mismatched parentheses in _logic expression')
|
|
21
|
+
}
|
|
22
|
+
pos++
|
|
23
|
+
return node
|
|
24
|
+
}
|
|
25
|
+
return { type: 'operand', value: tokens[pos++] }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return parseExpression()
|
|
29
|
+
}
|