@kava/kava-api-core 1.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/bun.lock +160 -0
- package/dist/auth.util.d.ts +23 -0
- package/dist/auth.util.js +175 -0
- package/dist/auth.util.js.map +1 -0
- package/dist/context.util.d.ts +7 -0
- package/dist/context.util.js +11 -0
- package/dist/context.util.js.map +1 -0
- package/dist/controller.util.d.ts +118 -0
- package/dist/controller.util.js +144 -0
- package/dist/controller.util.js.map +1 -0
- package/dist/conversion.util.d.ts +8 -0
- package/dist/conversion.util.js +52 -0
- package/dist/conversion.util.js.map +1 -0
- package/dist/db.util.d.ts +80 -0
- package/dist/db.util.js +166 -0
- package/dist/db.util.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.util.d.ts +30 -0
- package/dist/logger.util.js +117 -0
- package/dist/logger.util.js.map +1 -0
- package/dist/mail.util.d.ts +21 -0
- package/dist/mail.util.js +53 -0
- package/dist/mail.util.js.map +1 -0
- package/dist/middleware.util.d.ts +263 -0
- package/dist/middleware.util.js +233 -0
- package/dist/middleware.util.js.map +1 -0
- package/dist/model.util.d.ts +204 -0
- package/dist/model.util.js +1495 -0
- package/dist/model.util.js.map +1 -0
- package/dist/permission.util.d.ts +38 -0
- package/dist/permission.util.js +91 -0
- package/dist/permission.util.js.map +1 -0
- package/dist/route.util.d.ts +1 -0
- package/dist/route.util.js +12 -0
- package/dist/route.util.js.map +1 -0
- package/dist/storage.util.d.ts +56 -0
- package/dist/storage.util.js +82 -0
- package/dist/storage.util.js.map +1 -0
- package/dist/validation.util.d.ts +7 -0
- package/dist/validation.util.js +237 -0
- package/dist/validation.util.js.map +1 -0
- package/package.json +34 -0
- package/src/auth.util.ts +242 -0
- package/src/context.util.ts +17 -0
- package/src/controller.util.ts +237 -0
- package/src/conversion.util.ts +65 -0
- package/src/db.util.ts +405 -0
- package/src/index.ts +13 -0
- package/src/logger.util.ts +170 -0
- package/src/mail.util.ts +86 -0
- package/src/middleware.util.ts +289 -0
- package/src/model.util.ts +2211 -0
- package/src/permission.util.ts +136 -0
- package/src/route.util.ts +12 -0
- package/src/storage.util.ts +102 -0
- package/src/validation.util.ts +338 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import validator from "validator";
|
|
2
|
+
import { db } from "@utils";
|
|
3
|
+
// ==================================>
|
|
4
|
+
// ## Check validate field from rules
|
|
5
|
+
// ==================================>
|
|
6
|
+
export async function validate(data, rules) {
|
|
7
|
+
const errors = {};
|
|
8
|
+
for (const field in rules) {
|
|
9
|
+
const fieldRules = normalizeRules(rules[field]);
|
|
10
|
+
if (field.includes("*")) {
|
|
11
|
+
// const [arrayPath, childPath] = field.split(".*.")
|
|
12
|
+
// const arr = getNestedValue(data, arrayPath)
|
|
13
|
+
// if (!Array.isArray(arr)) {
|
|
14
|
+
// addError(errors, arrayPath, `${arrayPath} harus berupa array`)
|
|
15
|
+
// continue
|
|
16
|
+
// }
|
|
17
|
+
// for (let i = 0; i < arr.length; i++) {
|
|
18
|
+
// const value = childPath
|
|
19
|
+
// ? getNestedValue(arr[i], childPath)
|
|
20
|
+
// : arr[i]
|
|
21
|
+
// const itemField = childPath
|
|
22
|
+
// ? `${arrayPath}.${i}.${childPath}`
|
|
23
|
+
// : `${arrayPath}.${i}`
|
|
24
|
+
// await checkRules({ field: itemField, value, rules: fieldRules, data, errors })
|
|
25
|
+
// }
|
|
26
|
+
const segments = field.split(".");
|
|
27
|
+
await nestedValidation({ value: data, segments, rules: fieldRules, fieldPath: "", data, errors });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const value = getNestedValue(data, field) ?? "";
|
|
31
|
+
await checkRules({ field, value, rules: fieldRules, data, errors });
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
valid: Object.keys(errors).length === 0,
|
|
35
|
+
errors
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function checkRules({ field, value, rules, data, errors }) {
|
|
39
|
+
for (const rule of rules) {
|
|
40
|
+
const [name, param] = rule.split(":");
|
|
41
|
+
switch (name) {
|
|
42
|
+
// === BASIC ===
|
|
43
|
+
case "required":
|
|
44
|
+
if (validator.isEmpty(String(value).trim())) {
|
|
45
|
+
addError(errors, field, `${field} wajib diisi`);
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case "string":
|
|
49
|
+
case "text":
|
|
50
|
+
if (typeof value !== "string") {
|
|
51
|
+
addError(errors, field, `${field} harus berupa string`);
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case "numeric":
|
|
55
|
+
case "number":
|
|
56
|
+
if (!validator.isNumeric(String(value))) {
|
|
57
|
+
addError(errors, field, `${field} harus berupa angka`);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
case "boolean":
|
|
61
|
+
if (!(value === true || value === false || value === "true" || value === "false" || value === 1 || value === 0)) {
|
|
62
|
+
addError(errors, field, `${field} harus berupa boolean`);
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
case "email":
|
|
66
|
+
if (!validator.isEmail(String(value))) {
|
|
67
|
+
addError(errors, field, `${field} harus berupa email yang valid`);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case "url":
|
|
71
|
+
if (!validator.isURL(String(value))) {
|
|
72
|
+
addError(errors, field, `${field} harus berupa URL yang valid`);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case "date":
|
|
76
|
+
if (!validator.isDate(String(value))) {
|
|
77
|
+
addError(errors, field, `${field} harus berupa tanggal yang valid`);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
// === LENGTH ===
|
|
81
|
+
case "min": {
|
|
82
|
+
const min = parseInt(param);
|
|
83
|
+
if (!validator.isLength(String(value), { min })) {
|
|
84
|
+
addError(errors, field, `${field} minimal ${min} karakter`);
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "max": {
|
|
89
|
+
const max = parseInt(param);
|
|
90
|
+
if (!validator.isLength(String(value), { max })) {
|
|
91
|
+
addError(errors, field, `${field} maksimal ${max} karakter`);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "between": {
|
|
96
|
+
const [minVal, maxVal] = param.split(",").map(Number);
|
|
97
|
+
if (!validator.isLength(String(value), { min: minVal, max: maxVal })) {
|
|
98
|
+
addError(errors, field, `${field} harus antara ${minVal} - ${maxVal} karakter`);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
// === SET MEMBERSHIP ===
|
|
103
|
+
case "in": {
|
|
104
|
+
const allowed = param.split(",");
|
|
105
|
+
if (!allowed.includes(String(value))) {
|
|
106
|
+
addError(errors, field, `${field} harus salah satu dari: ${allowed.join(", ")}`);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "not_in": {
|
|
111
|
+
const notAllowed = param.split(",");
|
|
112
|
+
if (notAllowed.includes(String(value))) {
|
|
113
|
+
addError(errors, field, `${field} tidak boleh salah satu dari: ${notAllowed.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "array":
|
|
118
|
+
if (!Array.isArray(value)) {
|
|
119
|
+
addError(errors, field, `${field} harus berupa array`);
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
// === RELATIONAL ===
|
|
123
|
+
case "confirmed":
|
|
124
|
+
if (value !== getNestedValue(data, `${field}_confirmation`)) {
|
|
125
|
+
addError(errors, field, `${field} tidak sama dengan konfirmasi`);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
case "same":
|
|
129
|
+
if (value !== getNestedValue(data, param)) {
|
|
130
|
+
addError(errors, field, `${field} harus sama dengan ${param}`);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
case "different":
|
|
134
|
+
if (value === getNestedValue(data, param)) {
|
|
135
|
+
addError(errors, field, `${field} harus berbeda dengan ${param}`);
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
// === REGEX ===
|
|
139
|
+
case "regex":
|
|
140
|
+
try {
|
|
141
|
+
const pattern = new RegExp(param);
|
|
142
|
+
if (!pattern.test(String(value))) {
|
|
143
|
+
addError(errors, field, `${field} tidak sesuai format`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
addError(errors, field, `Regex rule untuk ${field} tidak valid`);
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
// === DATABASE VALIDATION ===
|
|
151
|
+
case "unique": {
|
|
152
|
+
const [table, column, exceptId] = param.split(",");
|
|
153
|
+
const query = db.table(table).where(column, value);
|
|
154
|
+
if (exceptId)
|
|
155
|
+
query.whereNot("id", exceptId);
|
|
156
|
+
const existing = await query.first();
|
|
157
|
+
if (existing) {
|
|
158
|
+
addError(errors, field, `${field} sudah digunakan`);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "exists": {
|
|
163
|
+
const [table, column] = param.split(",");
|
|
164
|
+
const existing = await db.table(table).where(column, value).first();
|
|
165
|
+
if (!existing) {
|
|
166
|
+
addError(errors, field, `${field} tidak ditemukan di ${table}`);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function nestedValidation({ value, segments, rules, fieldPath, data, errors }) {
|
|
174
|
+
if (segments.length === 0) {
|
|
175
|
+
await checkRules({
|
|
176
|
+
field: fieldPath,
|
|
177
|
+
value,
|
|
178
|
+
rules,
|
|
179
|
+
data,
|
|
180
|
+
errors
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const [segment, ...rest] = segments;
|
|
185
|
+
if (segment === "*") {
|
|
186
|
+
if (!Array.isArray(value)) {
|
|
187
|
+
addError(errors, fieldPath, `${fieldPath} harus berupa array`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (let i = 0; i < value.length; i++) {
|
|
191
|
+
await nestedValidation({
|
|
192
|
+
value: value[i],
|
|
193
|
+
segments: rest,
|
|
194
|
+
rules,
|
|
195
|
+
fieldPath: `${fieldPath}.${i}`,
|
|
196
|
+
data,
|
|
197
|
+
errors
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
await nestedValidation({
|
|
203
|
+
value: value?.[segment],
|
|
204
|
+
segments: rest,
|
|
205
|
+
rules,
|
|
206
|
+
fieldPath: fieldPath ? `${fieldPath}.${segment}` : segment,
|
|
207
|
+
data,
|
|
208
|
+
errors
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// ==================================>
|
|
213
|
+
// ## Validation helpers
|
|
214
|
+
// ==================================>
|
|
215
|
+
function getNestedValue(obj, path) {
|
|
216
|
+
if (!obj || typeof obj !== "object")
|
|
217
|
+
return undefined;
|
|
218
|
+
const normalizedPath = path
|
|
219
|
+
.replace(/\[(\w+)\]/g, '.$1')
|
|
220
|
+
.replace(/\['([^']+)'\]/g, '.$1')
|
|
221
|
+
.replace(/\["([^"]+)"\]/g, '.$1');
|
|
222
|
+
return normalizedPath.split('.').reduce((acc, key) => {
|
|
223
|
+
if (acc && Object.prototype.hasOwnProperty.call(acc, key)) {
|
|
224
|
+
return acc[key];
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}, obj);
|
|
228
|
+
}
|
|
229
|
+
function normalizeRules(rules) {
|
|
230
|
+
if (Array.isArray(rules))
|
|
231
|
+
return rules;
|
|
232
|
+
return rules.split("|");
|
|
233
|
+
}
|
|
234
|
+
function addError(errors, field, message) {
|
|
235
|
+
errors[field] = [...(errors[field] || []), message];
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=validation.util.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.util.js","sourceRoot":"","sources":["../src/validation.util.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kava/kava-api-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core utility functions for Kava API framework.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"aluna",
|
|
12
|
+
"core",
|
|
13
|
+
"utility"
|
|
14
|
+
],
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "UNLICENSED",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"bcrypt": "^6.0.0",
|
|
19
|
+
"dotenv": "^17.2.2",
|
|
20
|
+
"elysia": "latest",
|
|
21
|
+
"knex": "^3.1.0",
|
|
22
|
+
"nodemailer": "^7.0.9",
|
|
23
|
+
"pg": "^8.16.3",
|
|
24
|
+
"validator": "^13.15.15"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/bcrypt": "^6.0.0",
|
|
28
|
+
"@types/node": "^26.0.0",
|
|
29
|
+
"@types/nodemailer": "^7.0.2",
|
|
30
|
+
"@types/validator": "^13.15.3",
|
|
31
|
+
"bun-types": "latest",
|
|
32
|
+
"typescript": "^6.0.3"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/auth.util.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import bcrypt from "bcrypt";
|
|
3
|
+
import { db } from '@utils'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// =====================================>
|
|
8
|
+
// ## Auth: User Access Token
|
|
9
|
+
// =====================================>
|
|
10
|
+
const TOKEN_PLAIN_LENGTH = 20
|
|
11
|
+
const AUTH_PERMISSION = process.env.AUTH_CACHE === "true"
|
|
12
|
+
// const AUTH_CACHE = process.env.AUTH_CACHE === "true"
|
|
13
|
+
// const AUTH_CACHE_TTL = Number(process.env.AUTH_CACHE_TTL || 600)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export const auth = {
|
|
18
|
+
|
|
19
|
+
// =====================================>
|
|
20
|
+
// ## Auth: create access token with user id
|
|
21
|
+
// =====================================>
|
|
22
|
+
async createAccessToken(userId: number, req: Request, permission: boolean = true) {
|
|
23
|
+
const plain = crypto.randomBytes(TOKEN_PLAIN_LENGTH).toString("hex")
|
|
24
|
+
const hash = await bcrypt.hash(plain, 10)
|
|
25
|
+
const agent = generateAgentId(req)
|
|
26
|
+
|
|
27
|
+
let permissions: string[] = []
|
|
28
|
+
if (AUTH_PERMISSION && permission) {
|
|
29
|
+
permissions = await getUserPermissions(userId)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const [row] = await db("user_access_tokens").insert({
|
|
33
|
+
user_id : userId,
|
|
34
|
+
token : hash,
|
|
35
|
+
agent : agent,
|
|
36
|
+
permissions : JSON.stringify(permissions),
|
|
37
|
+
created_at : new Date(),
|
|
38
|
+
}).returning(["id"])
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
token : `${row.id}|${plain}`,
|
|
42
|
+
tokenId : row.id,
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// =====================================>
|
|
49
|
+
// ## Auth: delete access token with user id
|
|
50
|
+
// =====================================>
|
|
51
|
+
async revokeAccessToken(id: number) {
|
|
52
|
+
return db.table('user_access_tokens').where("id", id).delete()
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
// =====================================>
|
|
58
|
+
// ## Auth: verify access token
|
|
59
|
+
// =====================================>
|
|
60
|
+
async verifyAccessToken(token: string, req?: Request) {
|
|
61
|
+
if (!token.includes("|")) return null
|
|
62
|
+
|
|
63
|
+
const [tokenId, plain] = token.split("|", 2)
|
|
64
|
+
const agent = req ? generateAgentId(req) : ""
|
|
65
|
+
const ip = req ? getRequestIp(req) : ""
|
|
66
|
+
|
|
67
|
+
const cacheKey = `auth:token:${tokenId}`
|
|
68
|
+
|
|
69
|
+
// if (AUTH_CACHE) {
|
|
70
|
+
// const cached = await redis.get(cacheKey)
|
|
71
|
+
|
|
72
|
+
// if (cached) {
|
|
73
|
+
// const session = JSON.parse(cached)
|
|
74
|
+
|
|
75
|
+
// if (session.agent !== agent) return null
|
|
76
|
+
|
|
77
|
+
// return session
|
|
78
|
+
// }
|
|
79
|
+
// }
|
|
80
|
+
|
|
81
|
+
const tokenRecord = await db("user_access_tokens").where("id", tokenId).first()
|
|
82
|
+
|
|
83
|
+
if (!tokenRecord) return null
|
|
84
|
+
if (tokenRecord.agent !== agent) return null
|
|
85
|
+
|
|
86
|
+
const valid = await bcrypt.compare(plain, tokenRecord.token)
|
|
87
|
+
if (!valid) return null
|
|
88
|
+
|
|
89
|
+
await db("user_access_tokens").where("id", tokenRecord.id).update({ last_used_at: new Date(), last_used_ip: ip })
|
|
90
|
+
|
|
91
|
+
const user = await db("users").where("id", tokenRecord.user_id).first()
|
|
92
|
+
|
|
93
|
+
// if (AUTH_CACHE) {
|
|
94
|
+
// await redis.setex(
|
|
95
|
+
// cacheKey,
|
|
96
|
+
// AUTH_CACHE_TTL,
|
|
97
|
+
// JSON.stringify({
|
|
98
|
+
// user : user,
|
|
99
|
+
// agent : tokenRecord.agent,
|
|
100
|
+
// permissions : tokenRecord.permission,
|
|
101
|
+
// })
|
|
102
|
+
// )
|
|
103
|
+
// }
|
|
104
|
+
|
|
105
|
+
return { user, token: tokenRecord, permissions: tokenRecord.permission }
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
// =====================================>
|
|
111
|
+
// ## Auth: create user mail token
|
|
112
|
+
// =====================================>
|
|
113
|
+
async createUserMailToken(userId: number) {
|
|
114
|
+
const token = Math.floor(100000 + Math.random() * 900000).toString()
|
|
115
|
+
const hash = crypto.createHash('sha256').update(token).digest('hex')
|
|
116
|
+
|
|
117
|
+
const trx = await db.transaction()
|
|
118
|
+
|
|
119
|
+
await trx.table('user_mail_tokens').insert({
|
|
120
|
+
user_id : userId,
|
|
121
|
+
token : hash,
|
|
122
|
+
created_at : new Date(),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const record = await trx.table('user_mail_tokens').orderBy('id', 'desc').first()
|
|
126
|
+
|
|
127
|
+
await trx.commit()
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
token : token,
|
|
131
|
+
tokenId : record.id
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// =====================================>
|
|
138
|
+
// ## Auth: Verify user mail token
|
|
139
|
+
// =====================================>
|
|
140
|
+
async verifyUserMailToken(userId: number, token: string) {
|
|
141
|
+
const hashedToken = crypto.createHash("sha256").update(token).digest("hex");
|
|
142
|
+
|
|
143
|
+
const record = await db.table("user_mail_tokens")
|
|
144
|
+
.where("user_id", userId)
|
|
145
|
+
.whereNull("used_at")
|
|
146
|
+
.orderBy("id", "desc")
|
|
147
|
+
.first();
|
|
148
|
+
|
|
149
|
+
if (!record) return false
|
|
150
|
+
|
|
151
|
+
if (record.token !== hashedToken) return false;
|
|
152
|
+
|
|
153
|
+
const createdAt = new Date(record.created_at);
|
|
154
|
+
const now = new Date();
|
|
155
|
+
const diffMinutes = (now.getTime() - createdAt.getTime()) / (1000 * 60);
|
|
156
|
+
|
|
157
|
+
if (diffMinutes > 10) return false;
|
|
158
|
+
|
|
159
|
+
return true;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// =====================================>
|
|
165
|
+
// ## Auth: list user sessions
|
|
166
|
+
// =====================================>
|
|
167
|
+
async listUserSessions(userId: number, currentTokenId?: number) {
|
|
168
|
+
const rows = await db("user_access_tokens").select(["id", "agent", "created_at", "last_used_at", "last_used_ip","expired_at"]).where("user_id", userId).orderBy("last_used_at", "desc")
|
|
169
|
+
|
|
170
|
+
return rows.map(r => ({
|
|
171
|
+
...r,
|
|
172
|
+
is_active : r.revoked_at === null,
|
|
173
|
+
is_current : r.id === currentTokenId,
|
|
174
|
+
}))
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
// =====================================>
|
|
180
|
+
// ## Auth: revalidate user permission
|
|
181
|
+
// =====================================>
|
|
182
|
+
revalidateUserPermissions: revalidateUserPermissions,
|
|
183
|
+
revalidateUserPermissionsByRole: revalidateUserPermissionsByRole,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
function generateAgentId(req: Request) {
|
|
189
|
+
const ua = req.headers.get("user-agent") ?? ""
|
|
190
|
+
const acc = req.headers.get("accept") ?? ""
|
|
191
|
+
|
|
192
|
+
return crypto.createHash("sha256").update(ua + acc).digest("hex")
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
function getRequestIp(req: Request) {
|
|
197
|
+
return (req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async function getUserPermissions(userId: number): Promise<string[]> {
|
|
202
|
+
const roleIds = await db("user_roles").where("user_id", userId).pluck("role_id")
|
|
203
|
+
|
|
204
|
+
if (roleIds.length === 0) return []
|
|
205
|
+
|
|
206
|
+
const rows = await db("permissions").whereIn("role_id", roleIds).pluck("permissions")
|
|
207
|
+
|
|
208
|
+
return Array.from(
|
|
209
|
+
new Set(
|
|
210
|
+
rows.flatMap(p => p ?? [])
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async function revalidateUserPermissions(userId: number) {
|
|
217
|
+
const permissions = await getUserPermissions(userId)
|
|
218
|
+
|
|
219
|
+
const tokenIds = await db("user_access_tokens").where("user_id", userId).pluck("id")
|
|
220
|
+
|
|
221
|
+
if (tokenIds.length === 0) return
|
|
222
|
+
|
|
223
|
+
await db("user_access_tokens").whereIn("id", tokenIds).update({
|
|
224
|
+
permissions : JSON.stringify(permissions),
|
|
225
|
+
updated_at : new Date(),
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// if (AUTH_CACHE) {
|
|
229
|
+
// await Promise.all(
|
|
230
|
+
// tokenIds.map(id => redis.del(`auth:token:${id}`))
|
|
231
|
+
// )
|
|
232
|
+
// }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async function revalidateUserPermissionsByRole(roleId: number) {
|
|
237
|
+
const userIds = await db("user_roles").where("role_id", roleId).pluck("user_id")
|
|
238
|
+
|
|
239
|
+
// for (const userId of userIds) {
|
|
240
|
+
// await queue.add("auth:revalidate-permission", { userId })
|
|
241
|
+
// }
|
|
242
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
export type AppContext = {
|
|
4
|
+
user_id?: number,
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const storage = new AsyncLocalStorage<AppContext>()
|
|
8
|
+
|
|
9
|
+
export const context = {
|
|
10
|
+
run<T>(ctx: AppContext, fn: () => T) {
|
|
11
|
+
return storage.run(ctx, fn)
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
get<K extends keyof AppContext>(key: K): AppContext[K] {
|
|
15
|
+
return storage.getStore()?.[key]
|
|
16
|
+
},
|
|
17
|
+
}
|