nextly 0.0.1 → 0.0.2-alpha.1
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/LICENSE +22 -0
- package/README.md +122 -0
- package/dist/_dts-chunks/collections-handler.d-DjgO74Wt.d.ts +20540 -0
- package/dist/_dts-chunks/config.d-DNwsDnjs.d.ts +2589 -0
- package/dist/_dts-chunks/define-component.d-BUgTHmt3.d.ts +1149 -0
- package/dist/_dts-chunks/image-processor.d-OO1PmMrv.d.ts +335 -0
- package/dist/_dts-chunks/index.d-axCAzZ7m.d.ts +17842 -0
- package/dist/_dts-chunks/media.d-DjDOZo4B.d.ts +117 -0
- package/dist/_dts-chunks/on-error.d-CHIKWNxd.d.ts +38 -0
- package/dist/_dts-chunks/storage.d-BUhQ2we_.d.ts +404 -0
- package/dist/actions/index.d.ts +239 -0
- package/dist/actions/index.mjs +281 -0
- package/dist/api/auth-state.d.ts +5 -0
- package/dist/api/auth-state.mjs +131 -0
- package/dist/api/collections-schema-detail.d.ts +56 -0
- package/dist/api/collections-schema-detail.mjs +244 -0
- package/dist/api/collections-schema-export.d.ts +56 -0
- package/dist/api/collections-schema-export.mjs +129 -0
- package/dist/api/collections-schema.d.ts +59 -0
- package/dist/api/collections-schema.mjs +207 -0
- package/dist/api/components-detail.d.ts +50 -0
- package/dist/api/components-detail.mjs +132 -0
- package/dist/api/components.d.ts +69 -0
- package/dist/api/components.mjs +144 -0
- package/dist/api/email-providers-default.d.ts +40 -0
- package/dist/api/email-providers-default.mjs +75 -0
- package/dist/api/email-providers-detail.d.ts +81 -0
- package/dist/api/email-providers-detail.mjs +109 -0
- package/dist/api/email-providers-test.d.ts +43 -0
- package/dist/api/email-providers-test.mjs +114 -0
- package/dist/api/email-providers.d.ts +69 -0
- package/dist/api/email-providers.mjs +110 -0
- package/dist/api/email-send-template.d.ts +41 -0
- package/dist/api/email-send-template.mjs +58 -0
- package/dist/api/email-send.d.ts +42 -0
- package/dist/api/email-send.mjs +58 -0
- package/dist/api/email-templates-detail.d.ts +74 -0
- package/dist/api/email-templates-detail.mjs +112 -0
- package/dist/api/email-templates-layout.d.ts +55 -0
- package/dist/api/email-templates-layout.mjs +92 -0
- package/dist/api/email-templates-preview.d.ts +48 -0
- package/dist/api/email-templates-preview.mjs +93 -0
- package/dist/api/email-templates.d.ts +61 -0
- package/dist/api/email-templates.mjs +118 -0
- package/dist/api/health.d.ts +68 -0
- package/dist/api/health.mjs +67 -0
- package/dist/api/index.d.ts +54 -0
- package/dist/api/index.mjs +16 -0
- package/dist/api/media-bulk.d.ts +74 -0
- package/dist/api/media-bulk.mjs +196 -0
- package/dist/api/media-folders.d.ts +112 -0
- package/dist/api/media-folders.mjs +187 -0
- package/dist/api/media-handlers.d.ts +102 -0
- package/dist/api/media-handlers.mjs +437 -0
- package/dist/api/media.d.ts +117 -0
- package/dist/api/media.mjs +242 -0
- package/dist/api/singles-detail.d.ts +87 -0
- package/dist/api/singles-detail.mjs +170 -0
- package/dist/api/singles-schema-detail.d.ts +54 -0
- package/dist/api/singles-schema-detail.mjs +182 -0
- package/dist/api/singles.d.ts +34 -0
- package/dist/api/singles.mjs +94 -0
- package/dist/api/storage-upload-url.d.ts +48 -0
- package/dist/api/storage-upload-url.mjs +202 -0
- package/dist/api/uploads.d.ts +109 -0
- package/dist/api/uploads.mjs +359 -0
- package/dist/auth/index.d.ts +425 -0
- package/dist/auth/index.mjs +199 -0
- package/dist/boot-apply-PQSYLDIN.mjs +7 -0
- package/dist/chunk-2OALJTK6.mjs +489 -0
- package/dist/chunk-2Q2SX2CS.mjs +365 -0
- package/dist/chunk-2TFX4ND3.mjs +13 -0
- package/dist/chunk-2TWPDSYD.mjs +87 -0
- package/dist/chunk-2W3DVD7S.mjs +647 -0
- package/dist/chunk-2ZFKXPQM.mjs +88 -0
- package/dist/chunk-3FA7FKAV.mjs +832 -0
- package/dist/chunk-3NZ2KMBL.mjs +58 -0
- package/dist/chunk-4MJLT6PZ.mjs +0 -0
- package/dist/chunk-56WO4WX7.mjs +0 -0
- package/dist/chunk-5APFUGAD.mjs +89 -0
- package/dist/chunk-5HMZ644B.mjs +108 -0
- package/dist/chunk-67GXH6PR.mjs +32 -0
- package/dist/chunk-6JNEPWRW.mjs +14368 -0
- package/dist/chunk-6NFHQIJD.mjs +45 -0
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-A3WPLSDT.mjs +1364 -0
- package/dist/chunk-AGJ6F2T3.mjs +144 -0
- package/dist/chunk-AK6Z23OX.mjs +1464 -0
- package/dist/chunk-APKKRD2G.mjs +102 -0
- package/dist/chunk-B2GV2BWH.mjs +73 -0
- package/dist/chunk-D5HQBNUB.mjs +74 -0
- package/dist/chunk-DNNG377Z.mjs +204 -0
- package/dist/chunk-DP3G27G5.mjs +135 -0
- package/dist/chunk-DV6WVX2Q.mjs +0 -0
- package/dist/chunk-DXGGXIUZ.mjs +57 -0
- package/dist/chunk-EGXBZCGC.mjs +943 -0
- package/dist/chunk-ERCNLX3V.mjs +176 -0
- package/dist/chunk-FQULBZ53.mjs +850 -0
- package/dist/chunk-G2AA4QLC.mjs +262 -0
- package/dist/chunk-GDBJ5JCU.mjs +488 -0
- package/dist/chunk-GJNSJU4S.mjs +19 -0
- package/dist/chunk-GZ6DCQKC.mjs +69 -0
- package/dist/chunk-H26B4FYG.mjs +167 -0
- package/dist/chunk-I4JMR3UR.mjs +21 -0
- package/dist/chunk-INV7QKLG.mjs +508 -0
- package/dist/chunk-IUDOC7N7.mjs +46 -0
- package/dist/chunk-IZWPRDC3.mjs +206 -0
- package/dist/chunk-KIMNCZGV.mjs +15 -0
- package/dist/chunk-L6HW2DA7.mjs +15 -0
- package/dist/chunk-LAZXX4HR.mjs +100 -0
- package/dist/chunk-LDKCUMHK.mjs +95 -0
- package/dist/chunk-LRXMECUA.mjs +0 -0
- package/dist/chunk-M52VMPGA.mjs +119 -0
- package/dist/chunk-MGUWEEI6.mjs +160 -0
- package/dist/chunk-NRUWQ5Z7.mjs +419 -0
- package/dist/chunk-NSEFNNU4.mjs +25360 -0
- package/dist/chunk-NTHVDFGO.mjs +138 -0
- package/dist/chunk-O3QHXMOX.mjs +3166 -0
- package/dist/chunk-P7NH2OSC.mjs +2605 -0
- package/dist/chunk-PKMABBB5.mjs +184 -0
- package/dist/chunk-PWS6XGJK.mjs +76 -0
- package/dist/chunk-R6JJQHFC.mjs +20 -0
- package/dist/chunk-RJLLGGPG.mjs +0 -0
- package/dist/chunk-SBACDPNX.mjs +689 -0
- package/dist/chunk-TO5AFLVQ.mjs +124 -0
- package/dist/chunk-TS7GHTG2.mjs +5436 -0
- package/dist/chunk-UJ2IMJ4W.mjs +133 -0
- package/dist/chunk-UOP63Q54.mjs +102 -0
- package/dist/chunk-UUOFWCM6.mjs +78 -0
- package/dist/chunk-V4EQTOA4.mjs +893 -0
- package/dist/chunk-VJ66NCL4.mjs +193 -0
- package/dist/chunk-VQJQHVEV.mjs +29 -0
- package/dist/chunk-VTJADRO3.mjs +141 -0
- package/dist/chunk-VWF3JO32.mjs +0 -0
- package/dist/chunk-W4MGXIRR.mjs +27 -0
- package/dist/chunk-W5KKPZT5.mjs +1204 -0
- package/dist/chunk-WD34YQ6T.mjs +381 -0
- package/dist/chunk-WZBYMYVW.mjs +14 -0
- package/dist/chunk-X23WKS3Z.mjs +50 -0
- package/dist/chunk-X7TXCYYN.mjs +6496 -0
- package/dist/chunk-XGI4EMS3.mjs +140 -0
- package/dist/chunk-XZKLBMN6.mjs +1153 -0
- package/dist/chunk-YB7INWPY.mjs +0 -0
- package/dist/chunk-YV4Y7SDL.mjs +83 -0
- package/dist/chunk-YZNBLFIW.mjs +1688 -0
- package/dist/chunk-YZZCTONM.mjs +263 -0
- package/dist/chunk-ZE6A3FYH.mjs +289 -0
- package/dist/cli/nextly.mjs +68 -0
- package/dist/cli/utils/index.d.ts +449 -0
- package/dist/cli/utils/index.mjs +49 -0
- package/dist/component-schema-service-5577KVW6.mjs +11 -0
- package/dist/config-loader-23YEMC3Z.mjs +23 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.mjs +109 -0
- package/dist/container-ORGFGYSZ.mjs +9 -0
- package/dist/database/index.d.ts +12 -0
- package/dist/database/index.mjs +40 -0
- package/dist/database/seeders/index.d.ts +93 -0
- package/dist/database/seeders/index.mjs +47 -0
- package/dist/db-sync-demote-LJGKLB3S.mjs +117 -0
- package/dist/db-sync-promote-B26VSYQF.mjs +113 -0
- package/dist/dev-reload-broadcaster-B73IQ53V.mjs +25 -0
- package/dist/dist-M2NOU37V.mjs +19 -0
- package/dist/drizzle-kit-lazy-D2M2PXR2.mjs +13 -0
- package/dist/dynamic-collection-schema-service-IEXTPIZ7.mjs +8 -0
- package/dist/errors/index.d.ts +159 -0
- package/dist/errors/index.mjs +10 -0
- package/dist/factory-IWMBKUJM.mjs +15 -0
- package/dist/first-run-QIVKWJIF.mjs +63 -0
- package/dist/fresh-push-NR67DC3R.mjs +8 -0
- package/dist/index.d.ts +4175 -0
- package/dist/index.mjs +1336 -0
- package/dist/local-plugin-PTET4NAT.mjs +7 -0
- package/dist/logger-NU46DXNY.mjs +15 -0
- package/dist/logger-YE4TC7ZN.mjs +9 -0
- package/dist/migration-journal-EP532Y4L.mjs +139 -0
- package/dist/migrations/mysql/0000_eager_sentry.sql +174 -0
- package/dist/migrations/mysql/0001_soft_giant_girl.sql +27 -0
- package/dist/migrations/mysql/0002_media_table.sql +24 -0
- package/dist/migrations/mysql/0003_dynamic_singles.sql +37 -0
- package/dist/migrations/mysql/0004_dynamic_components.sql +35 -0
- package/dist/migrations/mysql/0005_user_management_tables.sql +92 -0
- package/dist/migrations/mysql/0006_api_keys.sql +36 -0
- package/dist/migrations/mysql/0007_general_settings.sql +20 -0
- package/dist/migrations/mysql/0008_site_settings_logo_url.sql +9 -0
- package/dist/migrations/mysql/0009_activity_log.sql +30 -0
- package/dist/migrations/mysql/0010_site_settings_sidebar.sql +13 -0
- package/dist/migrations/mysql/0011_missing_tables_and_columns.sql +54 -0
- package/dist/migrations/mysql/0012_image_sizes_and_focal_point.sql +30 -0
- package/dist/migrations/mysql/0012_media_folders.sql +43 -0
- package/dist/migrations/mysql/0013_user_brute_force_protection.sql +31 -0
- package/dist/migrations/mysql/0014_email_template_attachments.sql +12 -0
- package/dist/migrations/mysql/0015_media_uploaded_by_nullable.sql +15 -0
- package/dist/migrations/mysql/20260429_000000_000_initial_journal.sql +22 -0
- package/dist/migrations/mysql/20260501_000000_journal_batch.sql +17 -0
- package/dist/migrations/mysql/20260501_000001_audit_log.sql +24 -0
- package/dist/migrations/mysql/20260504_000000_nextly_meta.sql +21 -0
- package/dist/migrations/mysql/meta/0000_snapshot.json +1005 -0
- package/dist/migrations/mysql/meta/0001_snapshot.json +1099 -0
- package/dist/migrations/mysql/meta/_journal.json +41 -0
- package/dist/migrations/postgresql/0000_misty_king_bedlam.sql +169 -0
- package/dist/migrations/postgresql/0001_perpetual_captain_marvel.sql +8 -0
- package/dist/migrations/postgresql/0002_sad_spectrum.sql +16 -0
- package/dist/migrations/postgresql/0003_hesitant_ultron.sql +17 -0
- package/dist/migrations/postgresql/0004_media_table.sql +24 -0
- package/dist/migrations/postgresql/0005_media_folders.sql +36 -0
- package/dist/migrations/postgresql/0006_dynamic_collections_update.sql +50 -0
- package/dist/migrations/postgresql/0007_dynamic_singles.sql +38 -0
- package/dist/migrations/postgresql/0008_dynamic_components.sql +37 -0
- package/dist/migrations/postgresql/0009_user_management_tables.sql +95 -0
- package/dist/migrations/postgresql/0010_api_keys.sql +34 -0
- package/dist/migrations/postgresql/0011_general_settings.sql +20 -0
- package/dist/migrations/postgresql/0012_site_settings_logo_url.sql +9 -0
- package/dist/migrations/postgresql/0013_activity_log.sql +29 -0
- package/dist/migrations/postgresql/0014_image_sizes_and_focal_point.sql +33 -0
- package/dist/migrations/postgresql/0014_site_settings_sidebar.sql +13 -0
- package/dist/migrations/postgresql/0015_user_brute_force_protection.sql +29 -0
- package/dist/migrations/postgresql/0016_email_template_attachments.sql +12 -0
- package/dist/migrations/postgresql/0017_media_uploaded_by_nullable.sql +15 -0
- package/dist/migrations/postgresql/20260429_000000_000_initial_journal.sql +24 -0
- package/dist/migrations/postgresql/20260501_000000_journal_batch.sql +17 -0
- package/dist/migrations/postgresql/20260501_000001_audit_log.sql +24 -0
- package/dist/migrations/postgresql/20260504_000000_nextly_meta.sql +22 -0
- package/dist/migrations/postgresql/meta/0000_snapshot.json +1286 -0
- package/dist/migrations/postgresql/meta/0001_snapshot.json +1407 -0
- package/dist/migrations/postgresql/meta/0002_snapshot.json +1552 -0
- package/dist/migrations/postgresql/meta/0003_snapshot.json +1695 -0
- package/dist/migrations/postgresql/meta/0010_snapshot.json +2345 -0
- package/dist/migrations/postgresql/meta/_journal.json +90 -0
- package/dist/migrations/sqlite/0000_api_keys.sql +34 -0
- package/dist/migrations/sqlite/0001_general_settings.sql +20 -0
- package/dist/migrations/sqlite/0002_site_settings_logo_url.sql +9 -0
- package/dist/migrations/sqlite/0003_activity_log.sql +29 -0
- package/dist/migrations/sqlite/0004_image_sizes_and_focal_point.sql +29 -0
- package/dist/migrations/sqlite/0004_site_settings_sidebar.sql +11 -0
- package/dist/migrations/sqlite/0005_user_brute_force_protection.sql +29 -0
- package/dist/migrations/sqlite/0006_email_template_attachments.sql +12 -0
- package/dist/migrations/sqlite/0007_media_uploaded_by_nullable.sql +111 -0
- package/dist/migrations/sqlite/20260429_000000_000_initial_journal.sql +24 -0
- package/dist/migrations/sqlite/20260501_000000_journal_batch.sql +19 -0
- package/dist/migrations/sqlite/20260501_000001_audit_log.sql +24 -0
- package/dist/migrations/sqlite/20260504_000000_nextly_meta.sql +21 -0
- package/dist/migrations/sqlite/20260505_000000_user_management_tables.sql +77 -0
- package/dist/next.d.ts +57 -0
- package/dist/next.mjs +55 -0
- package/dist/observability/index.d.ts +87 -0
- package/dist/observability/index.mjs +57 -0
- package/dist/permissions-3DZZQZMI.mjs +39 -0
- package/dist/pipeline-YOML7SWF.mjs +29 -0
- package/dist/preview-ZZTR3QGS.mjs +9 -0
- package/dist/program-PW6UB2ZC.mjs +5934 -0
- package/dist/reconcile-single-tables-7ENVXJGB.mjs +7 -0
- package/dist/register-SF6E6FVU.mjs +49 -0
- package/dist/reload-config-HWQ4G5MM.mjs +23 -0
- package/dist/resolve-single-table-name-JSOMUB3R.mjs +7 -0
- package/dist/routeHandler-UNMMJIBM.mjs +77 -0
- package/dist/runtime-schema-generator-NRA6A6Z6.mjs +8 -0
- package/dist/runtime.d.ts +120 -0
- package/dist/runtime.mjs +73 -0
- package/dist/schema-hash-FMMG6VPJ.mjs +13 -0
- package/dist/schema-registry-EQ36FZDP.mjs +7 -0
- package/dist/scripts/load-env.mjs +42 -0
- package/dist/storage/index.d.ts +566 -0
- package/dist/storage/index.mjs +45 -0
- package/dist/super-admin-G5ZK5F4T.mjs +39 -0
- package/dist/system-table-service-WGSRVEGT.mjs +17 -0
- package/dist/users-7KELGRYJ.mjs +38 -0
- package/package.json +308 -9
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseService
|
|
3
|
+
} from "./chunk-2W3DVD7S.mjs";
|
|
4
|
+
import {
|
|
5
|
+
getDialectTables
|
|
6
|
+
} from "./chunk-TS7GHTG2.mjs";
|
|
7
|
+
import {
|
|
8
|
+
getAuthLogger
|
|
9
|
+
} from "./chunk-LAZXX4HR.mjs";
|
|
10
|
+
import {
|
|
11
|
+
container
|
|
12
|
+
} from "./chunk-D5HQBNUB.mjs";
|
|
13
|
+
|
|
14
|
+
// src/schemas/validation.ts
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var PaginationSchema = z.object({
|
|
17
|
+
page: z.number().int().min(1).default(1),
|
|
18
|
+
limit: z.number().int().min(1).max(100).default(10),
|
|
19
|
+
offset: z.number().int().min(0).optional()
|
|
20
|
+
});
|
|
21
|
+
var PaginatedResponseSchema = (dataSchema) => z.object({
|
|
22
|
+
data: z.array(dataSchema),
|
|
23
|
+
pagination: z.object({
|
|
24
|
+
page: z.number().int().min(1),
|
|
25
|
+
limit: z.number().int().min(1),
|
|
26
|
+
total: z.number().int().min(0),
|
|
27
|
+
totalPages: z.number().int().min(0),
|
|
28
|
+
hasNext: z.boolean(),
|
|
29
|
+
hasPrev: z.boolean()
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
var SortOrderSchema = z.enum(["asc", "desc"]).default("asc");
|
|
33
|
+
var SortSchema = z.object({
|
|
34
|
+
field: z.string().min(1, "Sort field is required"),
|
|
35
|
+
order: SortOrderSchema
|
|
36
|
+
});
|
|
37
|
+
var DateRangeSchema = z.object({
|
|
38
|
+
from: z.date().optional(),
|
|
39
|
+
to: z.date().optional()
|
|
40
|
+
}).refine((data) => !data.from || !data.to || data.from <= data.to, {
|
|
41
|
+
message: "From date must be before or equal to to date",
|
|
42
|
+
path: ["from"]
|
|
43
|
+
});
|
|
44
|
+
var SearchSchema = z.object({
|
|
45
|
+
query: z.string().min(1, "Search query is required").max(255, "Search query too long"),
|
|
46
|
+
fields: z.array(z.string()).optional()
|
|
47
|
+
});
|
|
48
|
+
var SuccessResponseSchema = z.object({
|
|
49
|
+
success: z.literal(true),
|
|
50
|
+
message: z.string().optional(),
|
|
51
|
+
data: z.any().optional()
|
|
52
|
+
});
|
|
53
|
+
var ErrorResponseSchema = z.object({
|
|
54
|
+
success: z.literal(false),
|
|
55
|
+
error: z.string(),
|
|
56
|
+
code: z.string().optional(),
|
|
57
|
+
details: z.any().optional()
|
|
58
|
+
});
|
|
59
|
+
var ValidationErrorSchema = z.object({
|
|
60
|
+
success: z.literal(false),
|
|
61
|
+
error: z.literal("Validation failed"),
|
|
62
|
+
details: z.array(
|
|
63
|
+
z.object({
|
|
64
|
+
field: z.string(),
|
|
65
|
+
message: z.string(),
|
|
66
|
+
code: z.string().optional()
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
});
|
|
70
|
+
var BulkOperationSchema = z.object({
|
|
71
|
+
ids: z.array(z.string()).min(1, "At least one ID is required"),
|
|
72
|
+
operation: z.enum(["delete", "update", "activate", "deactivate"])
|
|
73
|
+
});
|
|
74
|
+
var BulkOperationResponseSchema = z.object({
|
|
75
|
+
success: z.boolean(),
|
|
76
|
+
processed: z.number().int().min(0),
|
|
77
|
+
failed: z.number().int().min(0),
|
|
78
|
+
errors: z.array(
|
|
79
|
+
z.object({
|
|
80
|
+
id: z.string(),
|
|
81
|
+
error: z.string()
|
|
82
|
+
})
|
|
83
|
+
).optional()
|
|
84
|
+
});
|
|
85
|
+
var FileUploadSchema = z.object({
|
|
86
|
+
filename: z.string().min(1, "Filename is required"),
|
|
87
|
+
mimetype: z.string().min(1, "MIME type is required"),
|
|
88
|
+
size: z.number().int().min(1, "File size must be positive"),
|
|
89
|
+
buffer: z.instanceof(Buffer).optional()
|
|
90
|
+
});
|
|
91
|
+
var ImageUploadSchema = FileUploadSchema.extend({
|
|
92
|
+
mimetype: z.string().regex(/^image\//, "File must be an image"),
|
|
93
|
+
size: z.number().int().max(5 * 1024 * 1024, "Image size must be less than 5MB")
|
|
94
|
+
// 5MB limit
|
|
95
|
+
});
|
|
96
|
+
var EmailSchema = z.string().email("Invalid email format").transform((v) => v.trim().toLowerCase());
|
|
97
|
+
var PasswordSchema = z.string().min(8, "Password must be at least 8 characters").max(128, "Password must be less than 128 characters").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/\d/, "Password must contain at least one number").regex(
|
|
98
|
+
/[^A-Za-z0-9]/,
|
|
99
|
+
"Password must contain at least one special character"
|
|
100
|
+
);
|
|
101
|
+
var UrlSchema = z.string().url("Invalid URL format");
|
|
102
|
+
var PhoneSchema = z.string().regex(/^\+?[\d\s\-\\(\\)]+$/, "Invalid phone number format").min(10, "Phone number too short").max(20, "Phone number too long");
|
|
103
|
+
|
|
104
|
+
// src/auth/password/index.ts
|
|
105
|
+
import bcrypt from "bcryptjs";
|
|
106
|
+
var defaultSaltRounds = 12;
|
|
107
|
+
async function hashPassword(plain, saltRounds = defaultSaltRounds) {
|
|
108
|
+
if (!plain) {
|
|
109
|
+
throw new Error("hashPassword: plain must be non-empty");
|
|
110
|
+
}
|
|
111
|
+
const salt = await bcrypt.genSalt(saltRounds);
|
|
112
|
+
return bcrypt.hash(plain, salt);
|
|
113
|
+
}
|
|
114
|
+
async function verifyPassword(plain, hash) {
|
|
115
|
+
if (!plain || !hash) return false;
|
|
116
|
+
try {
|
|
117
|
+
return await bcrypt.compare(plain, hash);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function validatePasswordStrength(password) {
|
|
123
|
+
const result = PasswordSchema.safeParse(password);
|
|
124
|
+
if (result.success) {
|
|
125
|
+
return { ok: true };
|
|
126
|
+
} else {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
errors: result.error.issues.map(
|
|
130
|
+
(issue) => issue.message
|
|
131
|
+
)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/services/lib/permissions.ts
|
|
137
|
+
import { and as and3, eq as eq3, inArray as inArray3, ne } from "drizzle-orm";
|
|
138
|
+
|
|
139
|
+
// src/domains/auth/services/permission-cache-service.ts
|
|
140
|
+
import { and as and2, eq as eq2, gt, lt, sql } from "drizzle-orm";
|
|
141
|
+
|
|
142
|
+
// src/domains/auth/services/permission-checker-service.ts
|
|
143
|
+
import { inArray as inArray2 } from "drizzle-orm";
|
|
144
|
+
|
|
145
|
+
// src/domains/auth/services/role-inheritance-service.ts
|
|
146
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
147
|
+
var MAX_ROLE_HIERARCHY_DEPTH = 2e3;
|
|
148
|
+
var RoleInheritanceService = class extends BaseService {
|
|
149
|
+
constructor(adapter, logger) {
|
|
150
|
+
super(adapter, logger);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Add a parent-child role inheritance relationship.
|
|
154
|
+
*
|
|
155
|
+
* The child role will inherit all permissions from the parent role.
|
|
156
|
+
* Multi-level inheritance is supported.
|
|
157
|
+
*
|
|
158
|
+
* @param childRoleId - Child role ID (inherits from parent)
|
|
159
|
+
* @param parentRoleId - Parent role ID (provides permissions)
|
|
160
|
+
* @throws Error if self-inheritance or cycle would be created
|
|
161
|
+
*/
|
|
162
|
+
async addRoleInheritance(childRoleId, parentRoleId) {
|
|
163
|
+
if (childRoleId === parentRoleId) throw new Error("INHERIT_SELF_FORBIDDEN");
|
|
164
|
+
if (await this.willCreateCycle(childRoleId, parentRoleId))
|
|
165
|
+
throw new Error("INHERIT_CYCLE_FORBIDDEN");
|
|
166
|
+
const duplicate = await this.db.query.roleInherits.findFirst({
|
|
167
|
+
where: and(
|
|
168
|
+
eq(this.tables.roleInherits.parentRoleId, parentRoleId),
|
|
169
|
+
eq(this.tables.roleInherits.childRoleId, childRoleId)
|
|
170
|
+
),
|
|
171
|
+
columns: {
|
|
172
|
+
id: true
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (!duplicate) {
|
|
176
|
+
const id = `${parentRoleId}::${childRoleId}`;
|
|
177
|
+
try {
|
|
178
|
+
const inheritanceData = {
|
|
179
|
+
id,
|
|
180
|
+
childRoleId,
|
|
181
|
+
parentRoleId
|
|
182
|
+
};
|
|
183
|
+
const insert = this.db.insert(this.tables.roleInherits).values(inheritanceData);
|
|
184
|
+
if (typeof insert.onConflictDoNothing === "function") {
|
|
185
|
+
await insert.onConflictDoNothing();
|
|
186
|
+
} else {
|
|
187
|
+
await insert;
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
const error = e;
|
|
191
|
+
const code = String(error?.code || "");
|
|
192
|
+
const msg = String(error?.message || "").toLowerCase();
|
|
193
|
+
if (!(code === "ER_DUP_ENTRY" || msg.includes("duplicate"))) throw e;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
void invalidatePermissionCache({ roleId: childRoleId });
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Remove a parent-child role inheritance relationship.
|
|
200
|
+
*
|
|
201
|
+
* @param childRoleId - Child role ID
|
|
202
|
+
* @param parentRoleId - Parent role ID
|
|
203
|
+
*/
|
|
204
|
+
async removeRoleInheritance(childRoleId, parentRoleId) {
|
|
205
|
+
await this.db.delete(this.tables.roleInherits).where(
|
|
206
|
+
and(
|
|
207
|
+
eq(this.tables.roleInherits.childRoleId, childRoleId),
|
|
208
|
+
eq(this.tables.roleInherits.parentRoleId, parentRoleId)
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
void invalidatePermissionCache({ roleId: childRoleId });
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* List all ancestor roles (parents, grandparents, etc.) for a given role.
|
|
215
|
+
*
|
|
216
|
+
* Uses breadth-first traversal to find all roles in the hierarchy above this role.
|
|
217
|
+
* Note: The starting roleId is NOT included in the results - only its ancestors.
|
|
218
|
+
*
|
|
219
|
+
* Safety limit: Stops after visiting MAX_ROLE_HIERARCHY_DEPTH roles to prevent infinite loops.
|
|
220
|
+
*
|
|
221
|
+
* @param roleId - Role ID to find ancestors for
|
|
222
|
+
* @returns Array of ancestor role IDs (excluding the starting roleId)
|
|
223
|
+
*/
|
|
224
|
+
async listAncestorRoles(roleId) {
|
|
225
|
+
const visited = /* @__PURE__ */ new Set();
|
|
226
|
+
const queue = [roleId];
|
|
227
|
+
while (queue.length) {
|
|
228
|
+
const batch = queue.splice(0, 50);
|
|
229
|
+
const inheritances = await this.db.query.roleInherits.findMany({
|
|
230
|
+
where: inArray(this.tables.roleInherits.childRoleId, batch),
|
|
231
|
+
columns: {
|
|
232
|
+
parentRoleId: true
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
for (const r of inheritances) {
|
|
236
|
+
const parent = String(r.parentRoleId);
|
|
237
|
+
if (!visited.has(parent)) {
|
|
238
|
+
visited.add(parent);
|
|
239
|
+
queue.push(parent);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (visited.size > MAX_ROLE_HIERARCHY_DEPTH) break;
|
|
243
|
+
}
|
|
244
|
+
return Array.from(visited);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* List all descendant roles (children, grandchildren, etc.) for a given role.
|
|
248
|
+
*
|
|
249
|
+
* Uses breadth-first traversal to find all roles in the hierarchy below this role.
|
|
250
|
+
* Note: The starting roleId is NOT included in the results - only its descendants.
|
|
251
|
+
*
|
|
252
|
+
* Safety limit: Stops after visiting MAX_ROLE_HIERARCHY_DEPTH roles to prevent infinite loops.
|
|
253
|
+
*
|
|
254
|
+
* @param roleId - Role ID to find descendants for
|
|
255
|
+
* @returns Array of descendant role IDs (excluding the starting roleId)
|
|
256
|
+
*/
|
|
257
|
+
async listDescendantRoles(roleId) {
|
|
258
|
+
const visited = /* @__PURE__ */ new Set();
|
|
259
|
+
const queue = [roleId];
|
|
260
|
+
while (queue.length) {
|
|
261
|
+
const batch = queue.splice(0, 50);
|
|
262
|
+
const inheritances = await this.db.query.roleInherits.findMany({
|
|
263
|
+
where: inArray(this.tables.roleInherits.parentRoleId, batch),
|
|
264
|
+
columns: {
|
|
265
|
+
childRoleId: true
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
for (const r of inheritances) {
|
|
269
|
+
const child = String(r.childRoleId);
|
|
270
|
+
if (!visited.has(child)) {
|
|
271
|
+
visited.add(child);
|
|
272
|
+
queue.push(child);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (visited.size > MAX_ROLE_HIERARCHY_DEPTH) break;
|
|
276
|
+
}
|
|
277
|
+
return Array.from(visited);
|
|
278
|
+
}
|
|
279
|
+
async willCreateCycle(childRoleId, parentRoleId) {
|
|
280
|
+
const ancestorsOfParent = await this.listAncestorRoles(parentRoleId);
|
|
281
|
+
return ancestorsOfParent.includes(childRoleId);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/domains/auth/services/permission-checker-service.ts
|
|
286
|
+
var PermissionCheckerService = class extends BaseService {
|
|
287
|
+
roleInheritanceService;
|
|
288
|
+
constructor(adapter, logger) {
|
|
289
|
+
super(adapter, logger);
|
|
290
|
+
this.roleInheritanceService = new RoleInheritanceService(adapter, logger);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get all permissions for a given role (direct + inherited).
|
|
294
|
+
*
|
|
295
|
+
* @param roleId - Role ID to get permissions for
|
|
296
|
+
* @returns Array of permission IDs (deduplicated)
|
|
297
|
+
*/
|
|
298
|
+
async getAllPermissionsForRole(roleId) {
|
|
299
|
+
const childRoleIds = await this.roleInheritanceService.listDescendantRoles(roleId);
|
|
300
|
+
const allRoleIds = [roleId, ...childRoleIds];
|
|
301
|
+
const allPermissions = await this.db.select({
|
|
302
|
+
permissionId: this.tables.rolePermissions.permissionId
|
|
303
|
+
}).from(this.tables.rolePermissions).where(inArray2(this.tables.rolePermissions.roleId, allRoleIds));
|
|
304
|
+
const permissionSet = /* @__PURE__ */ new Set();
|
|
305
|
+
for (const perm of allPermissions) {
|
|
306
|
+
permissionSet.add(String(perm.permissionId));
|
|
307
|
+
}
|
|
308
|
+
return Array.from(permissionSet);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/domains/auth/services/permission-cache-service.ts
|
|
313
|
+
var errorLogRateLimiter = /* @__PURE__ */ new Map();
|
|
314
|
+
var ERROR_LOG_INTERVAL_MS = 6e4;
|
|
315
|
+
function shouldLogError(errorKey) {
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
const lastLogged = errorLogRateLimiter.get(errorKey);
|
|
318
|
+
if (!lastLogged || now - lastLogged > ERROR_LOG_INTERVAL_MS) {
|
|
319
|
+
errorLogRateLimiter.set(errorKey, now);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
var PermissionCacheService = class extends BaseService {
|
|
325
|
+
permissionChecker;
|
|
326
|
+
cacheTtlMs;
|
|
327
|
+
constructor(adapter, logger, options) {
|
|
328
|
+
super(adapter, logger);
|
|
329
|
+
this.permissionChecker = new PermissionCheckerService(adapter, logger);
|
|
330
|
+
const ttlSeconds = options?.cacheTtlSeconds ?? 86400;
|
|
331
|
+
this.cacheTtlMs = ttlSeconds * 1e3;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Serialize a `roleIds` array for the user_permission_cache.role_ids
|
|
335
|
+
* column.
|
|
336
|
+
*
|
|
337
|
+
* The column type differs by dialect:
|
|
338
|
+
* - PostgreSQL: jsonb (Drizzle stringifies the JS array)
|
|
339
|
+
* - MySQL: json (Drizzle stringifies the JS array)
|
|
340
|
+
* - SQLite: text (no automatic serialization; a raw array
|
|
341
|
+
* coerces via `String([a, b])` to "a,b" which
|
|
342
|
+
* is not valid JSON, breaking the
|
|
343
|
+
* `json_each(role_ids)` invalidation query)
|
|
344
|
+
*
|
|
345
|
+
* Match the column type per dialect so PG/MySQL keep the structured
|
|
346
|
+
* value and SQLite gets a JSON-parseable string.
|
|
347
|
+
*/
|
|
348
|
+
serializeRoleIdsForDb(roleIds) {
|
|
349
|
+
return this.dialect === "sqlite" ? JSON.stringify(roleIds) : roleIds;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Pre-compute and store all permissions for a user (cache warming).
|
|
353
|
+
*
|
|
354
|
+
* This method is called on:
|
|
355
|
+
* - User login
|
|
356
|
+
* - Role assignment changes
|
|
357
|
+
* - Permission changes affecting user's roles
|
|
358
|
+
*
|
|
359
|
+
* Performance: O(n) where n = number of possible permission checks (~20-50 typically)
|
|
360
|
+
*
|
|
361
|
+
* @param userId - User ID to warm cache for
|
|
362
|
+
* @returns Promise that resolves when cache is warmed
|
|
363
|
+
*/
|
|
364
|
+
async warmCacheForUser(userId) {
|
|
365
|
+
if (!userId) {
|
|
366
|
+
getAuthLogger()?.log?.("warn", {
|
|
367
|
+
category: "auth",
|
|
368
|
+
op: "cache",
|
|
369
|
+
message: "warmCacheForUser called with empty userId"
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const { permissions, userPermissionCache } = this.tables;
|
|
375
|
+
const allPermissions = await this.db.select({
|
|
376
|
+
id: permissions.id,
|
|
377
|
+
action: permissions.action,
|
|
378
|
+
resource: permissions.resource
|
|
379
|
+
}).from(permissions);
|
|
380
|
+
if (allPermissions.length === 0) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const roleIds = await this.permissionChecker.getAllPermissionsForRole(userId);
|
|
384
|
+
const expiresAt = new Date(Date.now() + this.cacheTtlMs);
|
|
385
|
+
const entries = [];
|
|
386
|
+
const serializedRoleIds = this.serializeRoleIdsForDb(Array.from(roleIds));
|
|
387
|
+
for (const perm of allPermissions) {
|
|
388
|
+
const hasPermission2 = roleIds.includes(perm.id);
|
|
389
|
+
const cacheKey = `${userId}|${perm.action}|${perm.resource}`;
|
|
390
|
+
entries.push({
|
|
391
|
+
id: cacheKey,
|
|
392
|
+
userId,
|
|
393
|
+
action: perm.action,
|
|
394
|
+
resource: perm.resource,
|
|
395
|
+
hasPermission: hasPermission2,
|
|
396
|
+
roleIds: serializedRoleIds,
|
|
397
|
+
expiresAt,
|
|
398
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (entries.length > 0) {
|
|
402
|
+
await this.db.insert(userPermissionCache).values(entries).onConflictDoUpdate({
|
|
403
|
+
target: userPermissionCache.id,
|
|
404
|
+
set: {
|
|
405
|
+
hasPermission: sql`EXCLUDED.has_permission`,
|
|
406
|
+
roleIds: sql`EXCLUDED.role_ids`,
|
|
407
|
+
expiresAt: sql`EXCLUDED.expires_at`,
|
|
408
|
+
createdAt: sql`EXCLUDED.created_at`
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (process.env.DEBUG_CACHE === "1") {
|
|
413
|
+
console.log("[cache][dbg] warmCacheForUser", {
|
|
414
|
+
userId,
|
|
415
|
+
entriesCount: entries.length,
|
|
416
|
+
ttlMs: this.cacheTtlMs
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
getAuthLogger()?.log?.("error", {
|
|
421
|
+
category: "auth",
|
|
422
|
+
op: "cache",
|
|
423
|
+
message: "warmCacheForUser failed",
|
|
424
|
+
userId,
|
|
425
|
+
error: String(error)
|
|
426
|
+
});
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get cached permission result from database.
|
|
432
|
+
*
|
|
433
|
+
* Returns:
|
|
434
|
+
* - `true` if permission is cached and granted
|
|
435
|
+
* - `false` if permission is cached and denied
|
|
436
|
+
* - `null` if cache miss (not cached or expired)
|
|
437
|
+
*
|
|
438
|
+
* Performance: <5ms (indexed lookup on composite key)
|
|
439
|
+
*
|
|
440
|
+
* @param userId - User ID
|
|
441
|
+
* @param action - Permission action (create, read, update, delete)
|
|
442
|
+
* @param resource - Permission resource (users, roles, permissions, etc.)
|
|
443
|
+
* @returns Promise resolving to boolean if cached, null if miss
|
|
444
|
+
*/
|
|
445
|
+
async getCachedPermission(userId, action, resource) {
|
|
446
|
+
if (!userId || !action || !resource) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
const { userPermissionCache } = this.tables;
|
|
451
|
+
const cacheKey = `${userId}|${action}|${resource}`;
|
|
452
|
+
const now = /* @__PURE__ */ new Date();
|
|
453
|
+
const result = await this.db.select({
|
|
454
|
+
hasPermission: userPermissionCache.hasPermission,
|
|
455
|
+
expiresAt: userPermissionCache.expiresAt
|
|
456
|
+
}).from(userPermissionCache).where(
|
|
457
|
+
and2(
|
|
458
|
+
eq2(userPermissionCache.id, cacheKey),
|
|
459
|
+
// gt() lets Drizzle convert `now` (Date) to the column's typed
|
|
460
|
+
// representation (epoch seconds for SQLite mode:"timestamp",
|
|
461
|
+
// native timestamp for PG/MySQL). Raw `sql\`${col} > ${now}\``
|
|
462
|
+
// bypassed that conversion and SQLite drivers refused to bind
|
|
463
|
+
// a Date object — `TypeError: SQLite3 can only bind numbers,
|
|
464
|
+
// strings, bigints, buffers, and null` on every authed request.
|
|
465
|
+
// Mirrors the same-file `cleanupExpired` pattern at line 481.
|
|
466
|
+
gt(userPermissionCache.expiresAt, now)
|
|
467
|
+
)
|
|
468
|
+
).limit(1);
|
|
469
|
+
if (result.length === 0) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
const cached = result[0];
|
|
473
|
+
if (process.env.DEBUG_CACHE === "1") {
|
|
474
|
+
console.log("[cache][dbg] getCachedPermission HIT", {
|
|
475
|
+
userId,
|
|
476
|
+
action,
|
|
477
|
+
resource,
|
|
478
|
+
hasPermission: cached.hasPermission,
|
|
479
|
+
expiresAt: cached.expiresAt
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return cached.hasPermission;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
getAuthLogger()?.log?.("error", {
|
|
485
|
+
category: "auth",
|
|
486
|
+
op: "cache",
|
|
487
|
+
message: "getCachedPermission failed",
|
|
488
|
+
userId,
|
|
489
|
+
action,
|
|
490
|
+
resource,
|
|
491
|
+
error: String(error)
|
|
492
|
+
});
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Store permission result in database cache.
|
|
498
|
+
*
|
|
499
|
+
* Uses UPSERT (INSERT ON CONFLICT) to handle concurrent writes.
|
|
500
|
+
*
|
|
501
|
+
* Performance: <3ms (single indexed insert with ON CONFLICT)
|
|
502
|
+
*
|
|
503
|
+
* @param userId - User ID
|
|
504
|
+
* @param action - Permission action
|
|
505
|
+
* @param resource - Permission resource
|
|
506
|
+
* @param hasPermission - Whether user has the permission
|
|
507
|
+
* @param roleIds - Role IDs involved (for invalidation)
|
|
508
|
+
* @returns Promise that resolves when cache is updated
|
|
509
|
+
*/
|
|
510
|
+
async setCachedPermission(userId, action, resource, hasPermission2, roleIds) {
|
|
511
|
+
if (!userId || !action || !resource) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const { userPermissionCache } = this.tables;
|
|
516
|
+
const cacheKey = `${userId}|${action}|${resource}`;
|
|
517
|
+
const expiresAt = new Date(Date.now() + this.cacheTtlMs);
|
|
518
|
+
await this.db.insert(userPermissionCache).values({
|
|
519
|
+
id: cacheKey,
|
|
520
|
+
userId,
|
|
521
|
+
action,
|
|
522
|
+
resource,
|
|
523
|
+
hasPermission: hasPermission2,
|
|
524
|
+
roleIds: this.serializeRoleIdsForDb(roleIds),
|
|
525
|
+
expiresAt,
|
|
526
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
527
|
+
}).onConflictDoUpdate({
|
|
528
|
+
target: userPermissionCache.id,
|
|
529
|
+
set: {
|
|
530
|
+
hasPermission: sql`EXCLUDED.has_permission`,
|
|
531
|
+
roleIds: sql`EXCLUDED.role_ids`,
|
|
532
|
+
expiresAt: sql`EXCLUDED.expires_at`,
|
|
533
|
+
createdAt: sql`EXCLUDED.created_at`
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
if (process.env.DEBUG_CACHE === "1") {
|
|
537
|
+
console.log("[cache][dbg] setCachedPermission", {
|
|
538
|
+
userId,
|
|
539
|
+
action,
|
|
540
|
+
resource,
|
|
541
|
+
hasPermission: hasPermission2,
|
|
542
|
+
roleIds,
|
|
543
|
+
expiresAt
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
} catch (error) {
|
|
547
|
+
const errorKey = `setCachedPermission:${userId}`;
|
|
548
|
+
if (shouldLogError(errorKey)) {
|
|
549
|
+
getAuthLogger()?.log?.("error", {
|
|
550
|
+
category: "auth",
|
|
551
|
+
op: "cache",
|
|
552
|
+
message: "setCachedPermission failed (rate-limited, showing 1/min max)",
|
|
553
|
+
userId,
|
|
554
|
+
action,
|
|
555
|
+
resource,
|
|
556
|
+
error: String(error)
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Invalidate all cached permissions for a user.
|
|
563
|
+
*
|
|
564
|
+
* Called on:
|
|
565
|
+
* - Role assignment/removal
|
|
566
|
+
* - User deactivation
|
|
567
|
+
* - Manual cache clear
|
|
568
|
+
*
|
|
569
|
+
* Uses write-through invalidation (tombstone pattern) to prevent race conditions:
|
|
570
|
+
* 1. Mark entries as expired (expiresAt = now) - creates tombstone
|
|
571
|
+
* 2. Any concurrent reads will see expired entries and recompute
|
|
572
|
+
* 3. Concurrent writes will overwrite tombstones with fresh data
|
|
573
|
+
*
|
|
574
|
+
* Performance: <10ms (indexed update by userId)
|
|
575
|
+
*
|
|
576
|
+
* @param userId - User ID to invalidate
|
|
577
|
+
* @returns Promise resolving to number of entries invalidated
|
|
578
|
+
*/
|
|
579
|
+
async invalidateByUser(userId) {
|
|
580
|
+
if (!userId) {
|
|
581
|
+
return 0;
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const { userPermissionCache } = this.tables;
|
|
585
|
+
const result = await this.db.update(userPermissionCache).set({ expiresAt: /* @__PURE__ */ new Date() }).where(eq2(userPermissionCache.userId, userId));
|
|
586
|
+
const invalidatedCount = result.rowCount ?? 0;
|
|
587
|
+
if (process.env.DEBUG_CACHE === "1") {
|
|
588
|
+
console.log("[cache][dbg] invalidateByUser", {
|
|
589
|
+
userId,
|
|
590
|
+
invalidatedCount,
|
|
591
|
+
method: "tombstone"
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return invalidatedCount;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
getAuthLogger()?.log?.("error", {
|
|
597
|
+
category: "auth",
|
|
598
|
+
op: "cache",
|
|
599
|
+
message: "invalidateByUser failed",
|
|
600
|
+
userId,
|
|
601
|
+
error: String(error)
|
|
602
|
+
});
|
|
603
|
+
return 0;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Invalidate cached permissions for all users with a specific role.
|
|
608
|
+
*
|
|
609
|
+
* Called on:
|
|
610
|
+
* - Role permission changes
|
|
611
|
+
* - Role deletion
|
|
612
|
+
* - Permission changes
|
|
613
|
+
*
|
|
614
|
+
* Uses write-through invalidation (tombstone pattern) to prevent race conditions:
|
|
615
|
+
* 1. Mark entries as expired (expiresAt = now) - creates tombstone
|
|
616
|
+
* 2. Any concurrent reads will see expired entries and recompute
|
|
617
|
+
* 3. Concurrent writes will overwrite tombstones with fresh data
|
|
618
|
+
*
|
|
619
|
+
* Uses JSONB contains operator to find affected users.
|
|
620
|
+
*
|
|
621
|
+
* Performance: O(n) where n = users with role (~10-500ms depending on data)
|
|
622
|
+
*
|
|
623
|
+
* @param roleId - Role ID to invalidate
|
|
624
|
+
* @returns Promise resolving to number of entries invalidated
|
|
625
|
+
*/
|
|
626
|
+
async invalidateByRole(roleId) {
|
|
627
|
+
if (!roleId) {
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const { userPermissionCache } = this.tables;
|
|
632
|
+
const containsRoleClause = this.dialect === "postgresql" ? sql`${userPermissionCache.roleIds} @> ${JSON.stringify([roleId])}` : this.dialect === "mysql" ? sql`JSON_CONTAINS(${userPermissionCache.roleIds}, ${JSON.stringify(roleId)})` : sql`EXISTS (SELECT 1 FROM json_each(${userPermissionCache.roleIds}) WHERE value = ${roleId})`;
|
|
633
|
+
const result = await this.db.update(userPermissionCache).set({ expiresAt: /* @__PURE__ */ new Date() }).where(containsRoleClause);
|
|
634
|
+
const invalidatedCount = result.rowCount ?? 0;
|
|
635
|
+
if (process.env.DEBUG_CACHE === "1") {
|
|
636
|
+
console.log("[cache][dbg] invalidateByRole", {
|
|
637
|
+
roleId,
|
|
638
|
+
invalidatedCount,
|
|
639
|
+
method: "tombstone"
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return invalidatedCount;
|
|
643
|
+
} catch (error) {
|
|
644
|
+
getAuthLogger()?.log?.("error", {
|
|
645
|
+
category: "auth",
|
|
646
|
+
op: "cache",
|
|
647
|
+
message: "invalidateByRole failed",
|
|
648
|
+
roleId,
|
|
649
|
+
error: String(error)
|
|
650
|
+
});
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Remove expired cache entries (background maintenance).
|
|
656
|
+
*
|
|
657
|
+
* Should run periodically via cron job (recommended: daily).
|
|
658
|
+
*
|
|
659
|
+
* Performance: <100ms for 10k entries, uses indexed scan on expiresAt
|
|
660
|
+
*
|
|
661
|
+
* @returns Promise resolving to number of entries deleted
|
|
662
|
+
*/
|
|
663
|
+
async cleanupExpired() {
|
|
664
|
+
try {
|
|
665
|
+
const { userPermissionCache } = this.tables;
|
|
666
|
+
const now = /* @__PURE__ */ new Date();
|
|
667
|
+
const result = await this.db.delete(userPermissionCache).where(lt(userPermissionCache.expiresAt, now));
|
|
668
|
+
const deletedCount = result.rowCount ?? 0;
|
|
669
|
+
if (process.env.DEBUG_CACHE === "1" || deletedCount > 0) {
|
|
670
|
+
console.log("[cache][info] cleanupExpired", {
|
|
671
|
+
deletedCount,
|
|
672
|
+
timestamp: now.toISOString()
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
return deletedCount;
|
|
676
|
+
} catch (error) {
|
|
677
|
+
getAuthLogger()?.log?.("error", {
|
|
678
|
+
category: "auth",
|
|
679
|
+
op: "cache",
|
|
680
|
+
message: "cleanupExpired failed",
|
|
681
|
+
error: String(error)
|
|
682
|
+
});
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/services/lib/permissions.ts
|
|
689
|
+
if (typeof window !== "undefined") {
|
|
690
|
+
throw new Error(
|
|
691
|
+
"[nextly] Direct API permissions module loaded in a browser context. Direct API is server-only \u2014 import only from Server Components, Route Handlers, or Server Actions, never from client components."
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
function getDb() {
|
|
695
|
+
const adapter = container.get("adapter");
|
|
696
|
+
return adapter.getDrizzle();
|
|
697
|
+
}
|
|
698
|
+
function getAdapter() {
|
|
699
|
+
return container.get("adapter");
|
|
700
|
+
}
|
|
701
|
+
function getLogger() {
|
|
702
|
+
return container.has("logger") ? container.get("logger") : console;
|
|
703
|
+
}
|
|
704
|
+
var CACHE_ENABLED = process.env.PERMISSION_CACHE_ENABLED !== "false" && process.env.PERMISSION_CACHE_ENABLED !== "0";
|
|
705
|
+
var CACHE_TTL_SECONDS = parseInt(
|
|
706
|
+
process.env.PERMISSION_CACHE_TTL_SECONDS || "86400",
|
|
707
|
+
10
|
|
708
|
+
);
|
|
709
|
+
var _dialectTables = null;
|
|
710
|
+
function getTablesLazy() {
|
|
711
|
+
if (!_dialectTables) {
|
|
712
|
+
_dialectTables = getDialectTables();
|
|
713
|
+
}
|
|
714
|
+
return _dialectTables;
|
|
715
|
+
}
|
|
716
|
+
var PermissionChecker = class {
|
|
717
|
+
memo = /* @__PURE__ */ new Map();
|
|
718
|
+
t = getTablesLazy();
|
|
719
|
+
cacheService = null;
|
|
720
|
+
constructor() {
|
|
721
|
+
this.t = getTablesLazy();
|
|
722
|
+
if (CACHE_ENABLED) {
|
|
723
|
+
try {
|
|
724
|
+
this.cacheService = new PermissionCacheService(
|
|
725
|
+
getAdapter(),
|
|
726
|
+
getLogger(),
|
|
727
|
+
{
|
|
728
|
+
cacheTtlSeconds: CACHE_TTL_SECONDS
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
} catch (error) {
|
|
732
|
+
getAuthLogger()?.log?.("warn", {
|
|
733
|
+
category: "auth",
|
|
734
|
+
op: "cache",
|
|
735
|
+
message: "Failed to initialize PermissionCacheService",
|
|
736
|
+
error: String(error)
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async hasPermission(userId, action, resource) {
|
|
742
|
+
if (!userId || !action || !resource) {
|
|
743
|
+
getAuthLogger()?.log?.("debug", {
|
|
744
|
+
category: "auth",
|
|
745
|
+
op: "error",
|
|
746
|
+
userId,
|
|
747
|
+
action,
|
|
748
|
+
resource
|
|
749
|
+
});
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
const key = `${userId}|${action}|${resource}`;
|
|
753
|
+
const cached = this.memo.get(key);
|
|
754
|
+
if (typeof cached === "boolean") return cached;
|
|
755
|
+
const hit = cache.get(key);
|
|
756
|
+
if (hit) {
|
|
757
|
+
if (hit.expiresAt > Date.now()) {
|
|
758
|
+
this.memo.set(key, hit.value);
|
|
759
|
+
cache.delete(key);
|
|
760
|
+
cache.set(key, hit);
|
|
761
|
+
return hit.value;
|
|
762
|
+
}
|
|
763
|
+
cache.delete(key);
|
|
764
|
+
const rids = keyToRoleIds.get(key);
|
|
765
|
+
keyToRoleIds.delete(key);
|
|
766
|
+
if (rids) for (const rid of rids) roleIdToKeys.get(rid)?.delete(key);
|
|
767
|
+
userIdToKeys.get(userId)?.delete(key);
|
|
768
|
+
}
|
|
769
|
+
if (this.cacheService) {
|
|
770
|
+
try {
|
|
771
|
+
const dbCached = await this.cacheService.getCachedPermission(
|
|
772
|
+
userId,
|
|
773
|
+
action,
|
|
774
|
+
resource
|
|
775
|
+
);
|
|
776
|
+
if (dbCached !== null) {
|
|
777
|
+
this.memo.set(key, dbCached);
|
|
778
|
+
setCacheEntry(key, dbCached, userId, []);
|
|
779
|
+
return dbCached;
|
|
780
|
+
}
|
|
781
|
+
} catch (error) {
|
|
782
|
+
getAuthLogger()?.log?.("warn", {
|
|
783
|
+
category: "auth",
|
|
784
|
+
op: "cache",
|
|
785
|
+
message: "DB cache lookup failed, falling back to fresh computation",
|
|
786
|
+
userId,
|
|
787
|
+
action,
|
|
788
|
+
resource,
|
|
789
|
+
error: String(error)
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
const roleIds = await this.getAllRoleIdsForUser(userId);
|
|
795
|
+
if (roleIds.size === 0) {
|
|
796
|
+
this.memo.set(key, false);
|
|
797
|
+
if (this.cacheService) {
|
|
798
|
+
void this.cacheService.setCachedPermission(
|
|
799
|
+
userId,
|
|
800
|
+
action,
|
|
801
|
+
resource,
|
|
802
|
+
false,
|
|
803
|
+
[]
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
const allowed = await this.roleSetHasPermission(
|
|
809
|
+
Array.from(roleIds),
|
|
810
|
+
action,
|
|
811
|
+
resource
|
|
812
|
+
);
|
|
813
|
+
this.memo.set(key, allowed);
|
|
814
|
+
setCacheEntry(key, allowed, userId, Array.from(roleIds));
|
|
815
|
+
if (this.cacheService) {
|
|
816
|
+
void this.cacheService.setCachedPermission(
|
|
817
|
+
userId,
|
|
818
|
+
action,
|
|
819
|
+
resource,
|
|
820
|
+
allowed,
|
|
821
|
+
Array.from(roleIds)
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
return allowed;
|
|
825
|
+
} catch {
|
|
826
|
+
getAuthLogger()?.log?.("error", {
|
|
827
|
+
category: "auth",
|
|
828
|
+
op: "error",
|
|
829
|
+
userId,
|
|
830
|
+
action,
|
|
831
|
+
resource
|
|
832
|
+
});
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
async hasAnyPermission(userId, checks) {
|
|
837
|
+
if (!userId || !Array.isArray(checks) || checks.length === 0) return false;
|
|
838
|
+
for (const c of checks) {
|
|
839
|
+
if (await this.hasPermission(userId, c.action, c.resource)) return true;
|
|
840
|
+
}
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
async hasAllPermissions(userId, checks) {
|
|
844
|
+
if (!userId || !Array.isArray(checks) || checks.length === 0) return false;
|
|
845
|
+
for (const c of checks) {
|
|
846
|
+
if (!await this.hasPermission(userId, c.action, c.resource))
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
851
|
+
async getAllRoleIdsForUser(userId) {
|
|
852
|
+
const direct = await this.getDirectRoleIds(userId);
|
|
853
|
+
if (direct.size === 0) return direct;
|
|
854
|
+
const all = new Set(direct);
|
|
855
|
+
const queue = Array.from(direct);
|
|
856
|
+
const visited = new Set(queue);
|
|
857
|
+
const { roleInherits } = this.t;
|
|
858
|
+
while (queue.length > 0) {
|
|
859
|
+
const batch = queue.splice(0, 50);
|
|
860
|
+
const parentRows = await getDb().select({ parentRoleId: roleInherits.parentRoleId }).from(roleInherits).where(inArray3(roleInherits.childRoleId, batch));
|
|
861
|
+
for (const r of parentRows) {
|
|
862
|
+
const parentRoleId = String(r.parentRoleId);
|
|
863
|
+
if (!visited.has(parentRoleId)) {
|
|
864
|
+
visited.add(parentRoleId);
|
|
865
|
+
all.add(parentRoleId);
|
|
866
|
+
queue.push(parentRoleId);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const childRows = await getDb().select({ childRoleId: roleInherits.childRoleId }).from(roleInherits).where(inArray3(roleInherits.parentRoleId, batch));
|
|
870
|
+
for (const r of childRows) {
|
|
871
|
+
const childRoleId = String(r.childRoleId);
|
|
872
|
+
if (!visited.has(childRoleId)) {
|
|
873
|
+
visited.add(childRoleId);
|
|
874
|
+
all.add(childRoleId);
|
|
875
|
+
queue.push(childRoleId);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (visited.size > 2e3) {
|
|
879
|
+
getAuthLogger()?.log?.("warn", {
|
|
880
|
+
category: "auth",
|
|
881
|
+
op: "error",
|
|
882
|
+
userId
|
|
883
|
+
});
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return all;
|
|
888
|
+
}
|
|
889
|
+
async getDirectRoleIds(userId) {
|
|
890
|
+
const { userRoles } = this.t;
|
|
891
|
+
const rows = await getDb().select({ roleId: userRoles.roleId }).from(userRoles).where(eq3(userRoles.userId, userId));
|
|
892
|
+
return new Set(
|
|
893
|
+
rows.map((r) => String(r.roleId))
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
async roleSetHasPermission(roleIds, action, resource) {
|
|
897
|
+
if (roleIds.length === 0) return false;
|
|
898
|
+
const { roles, rolePermissions, permissions } = this.t;
|
|
899
|
+
try {
|
|
900
|
+
const superAdmin = await getDb().select({ id: roles.id }).from(roles).where(and3(inArray3(roles.id, roleIds), eq3(roles.slug, "super-admin"))).limit(1);
|
|
901
|
+
if (superAdmin.length > 0) return true;
|
|
902
|
+
const perm = await getDb().select({ id: permissions.id }).from(permissions).where(
|
|
903
|
+
and3(
|
|
904
|
+
eq3(permissions.action, action),
|
|
905
|
+
eq3(permissions.resource, resource)
|
|
906
|
+
)
|
|
907
|
+
).limit(1);
|
|
908
|
+
const permId = perm?.[0]?.id ?? null;
|
|
909
|
+
if (!permId) return false;
|
|
910
|
+
const rows = await getDb().select({ id: rolePermissions.id }).from(rolePermissions).where(
|
|
911
|
+
and3(
|
|
912
|
+
inArray3(rolePermissions.roleId, roleIds),
|
|
913
|
+
eq3(rolePermissions.permissionId, permId)
|
|
914
|
+
)
|
|
915
|
+
).limit(1);
|
|
916
|
+
return rows.length > 0;
|
|
917
|
+
} catch {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
var cacheTtlMs = 6e4;
|
|
923
|
+
var cacheMaxEntries = parseInt(process.env.PERMISSION_CACHE_MEMORY_SIZE ?? "10000", 10) || 1e4;
|
|
924
|
+
var cache = /* @__PURE__ */ new Map();
|
|
925
|
+
var keyToRoleIds = /* @__PURE__ */ new Map();
|
|
926
|
+
var roleIdToKeys = /* @__PURE__ */ new Map();
|
|
927
|
+
var userIdToKeys = /* @__PURE__ */ new Map();
|
|
928
|
+
function setCacheEntry(key, value, userId, roleIds) {
|
|
929
|
+
if (cache.size >= cacheMaxEntries) {
|
|
930
|
+
const oldest = cache.keys().next().value;
|
|
931
|
+
if (oldest) {
|
|
932
|
+
cache.delete(oldest);
|
|
933
|
+
const rids = keyToRoleIds.get(oldest);
|
|
934
|
+
keyToRoleIds.delete(oldest);
|
|
935
|
+
if (rids) for (const rid of rids) roleIdToKeys.get(rid)?.delete(oldest);
|
|
936
|
+
const u = oldest.split("|", 1)[0];
|
|
937
|
+
userIdToKeys.get(u)?.delete(oldest);
|
|
938
|
+
if (userIdToKeys.get(u)?.size === 0) userIdToKeys.delete(u);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
cache.set(key, { value, expiresAt: Date.now() + cacheTtlMs });
|
|
942
|
+
const roleSet = new Set(roleIds);
|
|
943
|
+
keyToRoleIds.set(key, roleSet);
|
|
944
|
+
for (const rid of roleSet) {
|
|
945
|
+
if (!roleIdToKeys.has(rid)) roleIdToKeys.set(rid, /* @__PURE__ */ new Set());
|
|
946
|
+
roleIdToKeys.get(rid).add(key);
|
|
947
|
+
}
|
|
948
|
+
if (!userIdToKeys.has(userId)) userIdToKeys.set(userId, /* @__PURE__ */ new Set());
|
|
949
|
+
userIdToKeys.get(userId).add(key);
|
|
950
|
+
}
|
|
951
|
+
async function hasPermission(userId, action, resource) {
|
|
952
|
+
try {
|
|
953
|
+
const checker = new PermissionChecker();
|
|
954
|
+
return await checker.hasPermission(userId, action, resource);
|
|
955
|
+
} catch {
|
|
956
|
+
getAuthLogger()?.log?.("error", {
|
|
957
|
+
category: "auth",
|
|
958
|
+
op: "error",
|
|
959
|
+
userId,
|
|
960
|
+
action,
|
|
961
|
+
resource
|
|
962
|
+
});
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
async function hasAnyPermission(userId, checks) {
|
|
967
|
+
try {
|
|
968
|
+
const checker = new PermissionChecker();
|
|
969
|
+
return await checker.hasAnyPermission(userId, checks);
|
|
970
|
+
} catch {
|
|
971
|
+
getAuthLogger()?.log?.("error", { category: "auth", op: "error", userId });
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function listEffectivePermissions(userId) {
|
|
976
|
+
if (!userId) {
|
|
977
|
+
getAuthLogger()?.log?.("debug", {
|
|
978
|
+
category: "auth",
|
|
979
|
+
op: "permissions",
|
|
980
|
+
error: "missing userId"
|
|
981
|
+
});
|
|
982
|
+
return [];
|
|
983
|
+
}
|
|
984
|
+
try {
|
|
985
|
+
const checker = new PermissionChecker();
|
|
986
|
+
const roleIds = await checker.getAllRoleIdsForUser(userId);
|
|
987
|
+
if (roleIds.size === 0) {
|
|
988
|
+
return [];
|
|
989
|
+
}
|
|
990
|
+
const t = getTablesLazy();
|
|
991
|
+
const { rolePermissions, permissions } = t;
|
|
992
|
+
const rows = await getDb().select({
|
|
993
|
+
action: permissions.action,
|
|
994
|
+
resource: permissions.resource
|
|
995
|
+
}).from(rolePermissions).innerJoin(permissions, eq3(rolePermissions.permissionId, permissions.id)).where(inArray3(rolePermissions.roleId, Array.from(roleIds)));
|
|
996
|
+
const permissionStrings = /* @__PURE__ */ new Set();
|
|
997
|
+
for (const row of rows) {
|
|
998
|
+
permissionStrings.add(`${row.resource}:${row.action}`);
|
|
999
|
+
}
|
|
1000
|
+
const result = Array.from(permissionStrings).sort();
|
|
1001
|
+
if (process.env.DEBUG_RBAC === "1") {
|
|
1002
|
+
console.log("[permissions][dbg] listEffectivePermissions", {
|
|
1003
|
+
userId,
|
|
1004
|
+
roleCount: roleIds.size,
|
|
1005
|
+
permissionCount: result.length,
|
|
1006
|
+
permissions: result
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return result;
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
getAuthLogger()?.log?.("error", {
|
|
1012
|
+
category: "auth",
|
|
1013
|
+
op: "permissions",
|
|
1014
|
+
userId,
|
|
1015
|
+
error: String(error)
|
|
1016
|
+
});
|
|
1017
|
+
return [];
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async function invalidatePermissionCache(_hint = {}) {
|
|
1021
|
+
const { userId, roleId } = _hint || {};
|
|
1022
|
+
if (userId) {
|
|
1023
|
+
const keys = userIdToKeys.get(userId);
|
|
1024
|
+
if (keys) {
|
|
1025
|
+
for (const k of keys) {
|
|
1026
|
+
cache.delete(k);
|
|
1027
|
+
const rids = keyToRoleIds.get(k);
|
|
1028
|
+
keyToRoleIds.delete(k);
|
|
1029
|
+
if (rids) for (const rid of rids) roleIdToKeys.get(rid)?.delete(k);
|
|
1030
|
+
}
|
|
1031
|
+
userIdToKeys.delete(userId);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (roleId) {
|
|
1035
|
+
const keys = roleIdToKeys.get(roleId);
|
|
1036
|
+
if (keys) {
|
|
1037
|
+
for (const k of keys) {
|
|
1038
|
+
cache.delete(k);
|
|
1039
|
+
const rids = keyToRoleIds.get(k);
|
|
1040
|
+
keyToRoleIds.delete(k);
|
|
1041
|
+
if (rids) for (const rid of rids) roleIdToKeys.get(rid)?.delete(k);
|
|
1042
|
+
const uid = k.split("|", 1)[0];
|
|
1043
|
+
userIdToKeys.get(uid)?.delete(k);
|
|
1044
|
+
if (userIdToKeys.get(uid)?.size === 0) userIdToKeys.delete(uid);
|
|
1045
|
+
}
|
|
1046
|
+
roleIdToKeys.delete(roleId);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (CACHE_ENABLED) {
|
|
1050
|
+
try {
|
|
1051
|
+
const cacheService = new PermissionCacheService(
|
|
1052
|
+
getAdapter(),
|
|
1053
|
+
getLogger(),
|
|
1054
|
+
{
|
|
1055
|
+
cacheTtlSeconds: CACHE_TTL_SECONDS
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
if (userId) {
|
|
1059
|
+
await cacheService.invalidateByUser(userId);
|
|
1060
|
+
}
|
|
1061
|
+
if (roleId) {
|
|
1062
|
+
await cacheService.invalidateByRole(roleId);
|
|
1063
|
+
}
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
getAuthLogger()?.log?.("error", {
|
|
1066
|
+
category: "auth",
|
|
1067
|
+
op: "cache",
|
|
1068
|
+
message: "DB cache invalidation failed",
|
|
1069
|
+
userId,
|
|
1070
|
+
roleId,
|
|
1071
|
+
error: String(error)
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
var superAdminCache = /* @__PURE__ */ new Map();
|
|
1077
|
+
var SUPER_ADMIN_CACHE_TTL_MS = 6e4;
|
|
1078
|
+
async function isSuperAdmin(userId) {
|
|
1079
|
+
if (!userId) return false;
|
|
1080
|
+
const cached = superAdminCache.get(userId);
|
|
1081
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
1082
|
+
return cached.value;
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
const checker = new PermissionChecker();
|
|
1086
|
+
const roleIds = await checker.getAllRoleIdsForUser(userId);
|
|
1087
|
+
if (roleIds.size === 0) {
|
|
1088
|
+
superAdminCache.set(userId, {
|
|
1089
|
+
value: false,
|
|
1090
|
+
expiresAt: Date.now() + SUPER_ADMIN_CACHE_TTL_MS
|
|
1091
|
+
});
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
const t = getTablesLazy();
|
|
1095
|
+
const { roles } = t;
|
|
1096
|
+
const superAdmin = await getDb().select({ id: roles.id }).from(roles).where(
|
|
1097
|
+
and3(
|
|
1098
|
+
inArray3(roles.id, Array.from(roleIds)),
|
|
1099
|
+
eq3(roles.slug, "super-admin")
|
|
1100
|
+
)
|
|
1101
|
+
).limit(1);
|
|
1102
|
+
const result = superAdmin.length > 0;
|
|
1103
|
+
superAdminCache.set(userId, {
|
|
1104
|
+
value: result,
|
|
1105
|
+
expiresAt: Date.now() + SUPER_ADMIN_CACHE_TTL_MS
|
|
1106
|
+
});
|
|
1107
|
+
if (superAdminCache.size > 1e3) {
|
|
1108
|
+
const oldest = superAdminCache.keys().next().value;
|
|
1109
|
+
if (oldest) superAdminCache.delete(oldest);
|
|
1110
|
+
}
|
|
1111
|
+
return result;
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
getAuthLogger()?.log?.("error", {
|
|
1114
|
+
category: "auth",
|
|
1115
|
+
op: "permissions",
|
|
1116
|
+
userId,
|
|
1117
|
+
error: String(error)
|
|
1118
|
+
});
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
async function hasSuperAdminExcluding(excludeUserId) {
|
|
1123
|
+
if (!excludeUserId) return false;
|
|
1124
|
+
try {
|
|
1125
|
+
const t = getTablesLazy();
|
|
1126
|
+
const { roles, userRoles } = t;
|
|
1127
|
+
const superAdminRole = await getDb().select({ id: roles.id }).from(roles).where(eq3(roles.slug, "super-admin")).limit(1);
|
|
1128
|
+
if (superAdminRole.length === 0) return false;
|
|
1129
|
+
const superAdminRoleId = superAdminRole[0].id;
|
|
1130
|
+
const otherSuperAdmins = await getDb().select({ userId: userRoles.userId }).from(userRoles).where(
|
|
1131
|
+
and3(
|
|
1132
|
+
eq3(userRoles.roleId, superAdminRoleId),
|
|
1133
|
+
ne(userRoles.userId, excludeUserId)
|
|
1134
|
+
)
|
|
1135
|
+
).limit(1);
|
|
1136
|
+
return otherSuperAdmins.length > 0;
|
|
1137
|
+
} catch {
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
async function containsSuperAdminRole(roleIds) {
|
|
1142
|
+
if (!roleIds || roleIds.length === 0) return false;
|
|
1143
|
+
try {
|
|
1144
|
+
const t = getTablesLazy();
|
|
1145
|
+
const { roles } = t;
|
|
1146
|
+
const rows = await getDb().select({ id: roles.id }).from(roles).where(and3(inArray3(roles.id, roleIds), eq3(roles.slug, "super-admin"))).limit(1);
|
|
1147
|
+
return rows.length > 0;
|
|
1148
|
+
} catch {
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function listRoleSlugsForUser(userId) {
|
|
1153
|
+
if (!userId) return [];
|
|
1154
|
+
try {
|
|
1155
|
+
const checker = new PermissionChecker();
|
|
1156
|
+
const roleIds = await checker.getAllRoleIdsForUser(userId);
|
|
1157
|
+
if (roleIds.size === 0) return [];
|
|
1158
|
+
const t = getTablesLazy();
|
|
1159
|
+
const { roles } = t;
|
|
1160
|
+
const rows = await getDb().select({ slug: roles.slug }).from(roles).where(inArray3(roles.id, Array.from(roleIds)));
|
|
1161
|
+
return rows.map((r) => r.slug);
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
getAuthLogger()?.log?.("error", {
|
|
1164
|
+
category: "auth",
|
|
1165
|
+
op: "permissions",
|
|
1166
|
+
userId,
|
|
1167
|
+
error: String(error)
|
|
1168
|
+
});
|
|
1169
|
+
return [];
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export {
|
|
1174
|
+
PaginationSchema,
|
|
1175
|
+
PaginatedResponseSchema,
|
|
1176
|
+
SortOrderSchema,
|
|
1177
|
+
SortSchema,
|
|
1178
|
+
DateRangeSchema,
|
|
1179
|
+
SearchSchema,
|
|
1180
|
+
SuccessResponseSchema,
|
|
1181
|
+
ErrorResponseSchema,
|
|
1182
|
+
ValidationErrorSchema,
|
|
1183
|
+
BulkOperationSchema,
|
|
1184
|
+
BulkOperationResponseSchema,
|
|
1185
|
+
FileUploadSchema,
|
|
1186
|
+
ImageUploadSchema,
|
|
1187
|
+
EmailSchema,
|
|
1188
|
+
PasswordSchema,
|
|
1189
|
+
UrlSchema,
|
|
1190
|
+
PhoneSchema,
|
|
1191
|
+
hashPassword,
|
|
1192
|
+
verifyPassword,
|
|
1193
|
+
validatePasswordStrength,
|
|
1194
|
+
hasPermission,
|
|
1195
|
+
hasAnyPermission,
|
|
1196
|
+
listEffectivePermissions,
|
|
1197
|
+
invalidatePermissionCache,
|
|
1198
|
+
isSuperAdmin,
|
|
1199
|
+
hasSuperAdminExcluding,
|
|
1200
|
+
containsSuperAdminRole,
|
|
1201
|
+
listRoleSlugsForUser,
|
|
1202
|
+
RoleInheritanceService,
|
|
1203
|
+
PermissionCheckerService
|
|
1204
|
+
};
|