@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.
Files changed (59) hide show
  1. package/bun.lock +160 -0
  2. package/dist/auth.util.d.ts +23 -0
  3. package/dist/auth.util.js +175 -0
  4. package/dist/auth.util.js.map +1 -0
  5. package/dist/context.util.d.ts +7 -0
  6. package/dist/context.util.js +11 -0
  7. package/dist/context.util.js.map +1 -0
  8. package/dist/controller.util.d.ts +118 -0
  9. package/dist/controller.util.js +144 -0
  10. package/dist/controller.util.js.map +1 -0
  11. package/dist/conversion.util.d.ts +8 -0
  12. package/dist/conversion.util.js +52 -0
  13. package/dist/conversion.util.js.map +1 -0
  14. package/dist/db.util.d.ts +80 -0
  15. package/dist/db.util.js +166 -0
  16. package/dist/db.util.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +14 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/logger.util.d.ts +30 -0
  21. package/dist/logger.util.js +117 -0
  22. package/dist/logger.util.js.map +1 -0
  23. package/dist/mail.util.d.ts +21 -0
  24. package/dist/mail.util.js +53 -0
  25. package/dist/mail.util.js.map +1 -0
  26. package/dist/middleware.util.d.ts +263 -0
  27. package/dist/middleware.util.js +233 -0
  28. package/dist/middleware.util.js.map +1 -0
  29. package/dist/model.util.d.ts +204 -0
  30. package/dist/model.util.js +1495 -0
  31. package/dist/model.util.js.map +1 -0
  32. package/dist/permission.util.d.ts +38 -0
  33. package/dist/permission.util.js +91 -0
  34. package/dist/permission.util.js.map +1 -0
  35. package/dist/route.util.d.ts +1 -0
  36. package/dist/route.util.js +12 -0
  37. package/dist/route.util.js.map +1 -0
  38. package/dist/storage.util.d.ts +56 -0
  39. package/dist/storage.util.js +82 -0
  40. package/dist/storage.util.js.map +1 -0
  41. package/dist/validation.util.d.ts +7 -0
  42. package/dist/validation.util.js +237 -0
  43. package/dist/validation.util.js.map +1 -0
  44. package/package.json +34 -0
  45. package/src/auth.util.ts +242 -0
  46. package/src/context.util.ts +17 -0
  47. package/src/controller.util.ts +237 -0
  48. package/src/conversion.util.ts +65 -0
  49. package/src/db.util.ts +405 -0
  50. package/src/index.ts +13 -0
  51. package/src/logger.util.ts +170 -0
  52. package/src/mail.util.ts +86 -0
  53. package/src/middleware.util.ts +289 -0
  54. package/src/model.util.ts +2211 -0
  55. package/src/permission.util.ts +136 -0
  56. package/src/route.util.ts +12 -0
  57. package/src/storage.util.ts +102 -0
  58. package/src/validation.util.ts +338 -0
  59. 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":"AAAA,OAAO,SAAS,MAAM,WAAW,CAAA;AACjC,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AA6C3B,sCAAsC;AACtC,qCAAqC;AACrC,sCAAsC;AACtC,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,IAAyB,EACzB,KAAsB;IAEtB,MAAM,MAAM,GAA6B,EAAE,CAAA;IAE3C,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;QAE/C,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,oDAAoD;YACpD,8CAA8C;YAE9C,6BAA6B;YAC7B,mEAAmE;YACnE,aAAa;YACb,IAAI;YAEJ,yCAAyC;YACzC,4BAA4B;YAC5B,0CAA0C;YAC1C,eAAe;YAEf,gCAAgC;YAChC,yCAAyC;YACzC,4BAA4B;YAE5B,mFAAmF;YACnF,IAAI;YACJ,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAEjC,MAAM,gBAAgB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;YAEjG,SAAQ;QACV,CAAC;QAGD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAA;QAE/C,MAAM,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;IACrE,CAAC;IAED,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;QACvC,MAAM;KACP,CAAA;AACH,CAAC;AAGD,KAAK,UAAU,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAwG;IACnK,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiC,CAAA;QAErE,QAAQ,IAAI,EAAE,CAAC;YACb,gBAAgB;YAChB,KAAK,UAAU;gBACb,IAAI,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;oBAC5C,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,cAAc,CAAC,CAAA;gBACjD,CAAC;gBACD,MAAK;YAEP,KAAK,QAAQ,CAAC;YACd,KAAK,MAAM;gBACT,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;oBAC9B,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,sBAAsB,CAAC,CAAA;gBACzD,CAAC;gBACD,MAAK;YAEP,KAAK,SAAS,CAAC;YACf,KAAK,QAAQ;gBACX,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBACxC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,qBAAqB,CAAC,CAAA;gBACxD,CAAC;gBACD,MAAK;YAEP,KAAK,SAAS;gBACZ,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;oBAChH,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,uBAAuB,CAAC,CAAA;gBAC1D,CAAC;gBACD,MAAK;YAEP,KAAK,OAAO;gBACV,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBACtC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,gCAAgC,CAAC,CAAA;gBACnE,CAAC;gBACD,MAAK;YAEP,KAAK,KAAK;gBACR,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBACpC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,8BAA8B,CAAC,CAAA;gBACjE,CAAC;gBACD,MAAK;YAEP,KAAK,MAAM;gBACT,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBACrC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,kCAAkC,CAAC,CAAA;gBACrE,CAAC;gBACD,MAAK;YAEP,iBAAiB;YACjB,KAAK,KAAK,CAAC,CAAC,CAAC;gBACX,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAM,CAAC,CAAA;gBAC5B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;oBAChD,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,YAAY,GAAG,WAAW,CAAC,CAAA;gBAC7D,CAAC;gBACD,MAAK;YACP,CAAC;YAED,KAAK,KAAK,CAAC,CAAC,CAAC;gBACX,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAM,CAAC,CAAA;gBAC5B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;oBAChD,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,aAAa,GAAG,WAAW,CAAC,CAAA;gBAC9D,CAAC;gBACD,MAAK;YACP,CAAC;YAED,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,KAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;gBACtD,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;oBACrE,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,iBAAiB,MAAM,MAAM,MAAM,WAAW,CAAC,CAAA;gBACjF,CAAC;gBACD,MAAK;YACP,CAAC;YAED,yBAAyB;YACzB,KAAK,IAAI,CAAC,CAAC,CAAC;gBACV,MAAM,OAAO,GAAG,KAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;gBACjC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBACrC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,2BAA2B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gBAClF,CAAC;gBACD,MAAK;YACP,CAAC;YAED,KAAK,QAAQ,CAAC,CAAC,CAAC;gBACd,MAAM,UAAU,GAAG,KAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;gBACpC,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBACvC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,iCAAiC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gBAC3F,CAAC;gBACD,MAAK;YACP,CAAC;YAED,KAAK,OAAO;gBACZ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC1B,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,qBAAqB,CAAC,CAAA;gBACxD,CAAC;gBACD,MAAK;YAEL,qBAAqB;YACrB,KAAK,WAAW;gBACd,IAAI,KAAK,KAAK,cAAc,CAAC,IAAI,EAAE,GAAG,KAAK,eAAe,CAAC,EAAE,CAAC;oBAC5D,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,+BAA+B,CAAC,CAAA;gBAClE,CAAC;gBACD,MAAK;YAEP,KAAK,MAAM;gBACT,IAAI,KAAK,KAAK,cAAc,CAAC,IAAI,EAAE,KAAM,CAAC,EAAE,CAAC;oBAC3C,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,sBAAsB,KAAK,EAAE,CAAC,CAAA;gBAChE,CAAC;gBACD,MAAK;YAEP,KAAK,WAAW;gBACd,IAAI,KAAK,KAAK,cAAc,CAAC,IAAI,EAAE,KAAM,CAAC,EAAE,CAAC;oBAC3C,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,yBAAyB,KAAK,EAAE,CAAC,CAAA;gBACnE,CAAC;gBACD,MAAK;YAEP,gBAAgB;YAChB,KAAK,OAAO;gBACV,IAAI,CAAC;oBACH,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,KAAM,CAAC,CAAA;oBAClC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;wBACjC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,sBAAsB,CAAC,CAAA;oBACzD,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,oBAAoB,KAAK,cAAc,CAAC,CAAA;gBAClE,CAAC;gBACD,MAAK;YAEP,8BAA8B;YAC9B,KAAK,QAAQ,CAAC,CAAC,CAAC;gBACd,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,KAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;gBACnD,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;gBAClD,IAAI,QAAQ;oBAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;gBAC5C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,CAAA;gBACpC,IAAI,QAAQ,EAAE,CAAC;oBACb,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,kBAAkB,CAAC,CAAA;gBACrD,CAAC;gBACD,MAAK;YACP,CAAC;YAED,KAAK,QAAQ,CAAC,CAAC,CAAC;gBACd,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,KAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;gBACzC,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,KAAK,EAAE,CAAA;gBACnE,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACd,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,uBAAuB,KAAK,EAAE,CAAC,CAAA;gBACjE,CAAC;gBACD,MAAK;YACP,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAID,KAAK,UAAU,gBAAgB,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,SAAS,EACT,IAAI,EACJ,MAAM,EAQP;IACC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,UAAU,CAAC;YACf,KAAK,EAAE,SAAS;YAChB,KAAK;YACL,KAAK;YACL,IAAI;YACJ,MAAM;SACP,CAAC,CAAA;QACF,OAAM;IACR,CAAC;IAED,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,QAAQ,CAAA;IAEnC,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,SAAS,qBAAqB,CAAC,CAAA;YAC9D,OAAM;QACR,CAAC;QAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,gBAAgB,CAAC;gBACrB,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;gBACf,QAAQ,EAAE,IAAI;gBACd,KAAK;gBACL,SAAS,EAAE,GAAG,SAAS,IAAI,CAAC,EAAE;gBAC9B,IAAI;gBACJ,MAAM;aACP,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,gBAAgB,CAAC;YACrB,KAAK,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC;YACvB,QAAQ,EAAE,IAAI;YACd,KAAK;YACL,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO;YAC1D,IAAI;YACJ,MAAM;SACP,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAID,sCAAsC;AACtC,wBAAwB;AACxB,sCAAsC;AACtC,SAAS,cAAc,CAAC,GAAQ,EAAE,IAAY;IAC5C,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAErD,MAAM,cAAc,GAAG,IAAI;SACxB,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC;SAC5B,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC;SAChC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;IAEnC,OAAO,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACnD,IAAI,GAAG,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;YAC1D,OAAO,GAAG,CAAC,GAAG,CAAC,CAAA;QACjB,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC,EAAE,GAAG,CAAC,CAAA;AACT,CAAC;AAED,SAAS,cAAc,CAAC,KAAgC;IACtD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACtC,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAqB,CAAA;AAC7C,CAAC;AAED,SAAS,QAAQ,CAAC,MAAgC,EAAE,KAAa,EAAE,OAAe;IAChF,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,CAAA;AACrD,CAAC"}
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
+ }
@@ -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
+ }