@kyro-cms/admin 0.1.2

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 (102) hide show
  1. package/.astro/content.d.ts +154 -0
  2. package/.astro/settings.json +5 -0
  3. package/.astro/types.d.ts +2 -0
  4. package/astro.config.mjs +28 -0
  5. package/bun.lock +1374 -0
  6. package/dist/client/_astro/AdminLayout.DkDpng53.css +1 -0
  7. package/dist/client/_astro/AutoForm.3eJCmCJp.js +1 -0
  8. package/dist/client/_astro/client.DyczpTbx.js +9 -0
  9. package/dist/client/_astro/index.B02hbnpo.js +1 -0
  10. package/dist/client/fonts/Serotiva-Black.woff2 +0 -0
  11. package/dist/client/fonts/Serotiva-Bold.woff2 +0 -0
  12. package/dist/client/fonts/Serotiva-Medium.woff2 +0 -0
  13. package/dist/client/fonts/Serotiva-Regular.woff2 +0 -0
  14. package/dist/client/fonts/Serotiva-SemiBold.woff2 +0 -0
  15. package/dist/server/chunks/AdminLayout_D-_JeUqC.mjs +26 -0
  16. package/dist/server/chunks/_id__BzI_o0qT.mjs +50 -0
  17. package/dist/server/chunks/_id__Cd-jOuY3.mjs +238 -0
  18. package/dist/server/chunks/_id__DvbD--iR.mjs +992 -0
  19. package/dist/server/chunks/_id__vpVaEo16.mjs +128 -0
  20. package/dist/server/chunks/_virtual_astro_server-island-manifest_CQQ1F5PF.mjs +7 -0
  21. package/dist/server/chunks/_virtual_astro_session-driver_Bk3Q189E.mjs +4 -0
  22. package/dist/server/chunks/astro-component_Dbx3T2Nh.mjs +37 -0
  23. package/dist/server/chunks/audit-logs_DrnUMRvY.mjs +74 -0
  24. package/dist/server/chunks/config_CPXslElD.mjs +4221 -0
  25. package/dist/server/chunks/dataStore_Dl7cA2Qp.mjs +89 -0
  26. package/dist/server/chunks/index_CVqOkerS.mjs +2960 -0
  27. package/dist/server/chunks/index_CX8SQ4BF.mjs +55 -0
  28. package/dist/server/chunks/index_CYofDU51.mjs +58 -0
  29. package/dist/server/chunks/index_DdNRhuaM.mjs +55 -0
  30. package/dist/server/chunks/index_DupPvtIF.mjs +42 -0
  31. package/dist/server/chunks/index_YTS_M-B9.mjs +263 -0
  32. package/dist/server/chunks/index_YeCzuVps.mjs +53 -0
  33. package/dist/server/chunks/login_DLyqMRO8.mjs +93 -0
  34. package/dist/server/chunks/logout_CSbt5wea.mjs +50 -0
  35. package/dist/server/chunks/me_C04jlYhH.mjs +41 -0
  36. package/dist/server/chunks/new_BbQ9b55M.mjs +92 -0
  37. package/dist/server/chunks/node_9bvTewss.mjs +1014 -0
  38. package/dist/server/chunks/noop-entrypoint_BOlrdqWF.mjs +3 -0
  39. package/dist/server/chunks/sequence_9cl7AJy-.mjs +2503 -0
  40. package/dist/server/chunks/server_peBx9VXG.mjs +8117 -0
  41. package/dist/server/chunks/sharp_pmJ7nHES.mjs +142 -0
  42. package/dist/server/chunks/users_Dzddy_YR.mjs +137 -0
  43. package/dist/server/entry.mjs +5 -0
  44. package/dist/server/virtual_astro_middleware.mjs +48 -0
  45. package/package.json +33 -0
  46. package/public/fonts/Serotiva-Black.woff2 +0 -0
  47. package/public/fonts/Serotiva-Bold.woff2 +0 -0
  48. package/public/fonts/Serotiva-Medium.woff2 +0 -0
  49. package/public/fonts/Serotiva-Regular.woff2 +0 -0
  50. package/public/fonts/Serotiva-SemiBold.woff2 +0 -0
  51. package/src/collections/auth/index.ts +155 -0
  52. package/src/components/ActionBar.tsx +215 -0
  53. package/src/components/Admin.tsx +214 -0
  54. package/src/components/AutoForm.tsx +1123 -0
  55. package/src/components/BulkActionsBar.tsx +80 -0
  56. package/src/components/CreateView.tsx +99 -0
  57. package/src/components/DetailView.tsx +329 -0
  58. package/src/components/Icons.tsx +23 -0
  59. package/src/components/ListView.tsx +192 -0
  60. package/src/components/StatusBadge.tsx +76 -0
  61. package/src/components/ThemeProvider.tsx +155 -0
  62. package/src/components/VersionHistoryPanel.tsx +205 -0
  63. package/src/components/fields/CheckboxField.tsx +37 -0
  64. package/src/components/fields/DateField.tsx +42 -0
  65. package/src/components/fields/NumberField.tsx +44 -0
  66. package/src/components/fields/RelationshipField.tsx +87 -0
  67. package/src/components/fields/SelectField.tsx +56 -0
  68. package/src/components/fields/TextField.tsx +49 -0
  69. package/src/components/index.ts +30 -0
  70. package/src/components/layout/Breadcrumbs.tsx +36 -0
  71. package/src/components/layout/Header.tsx +37 -0
  72. package/src/components/layout/Layout.tsx +25 -0
  73. package/src/components/layout/Sidebar.tsx +462 -0
  74. package/src/components/ui/Badge.tsx +14 -0
  75. package/src/components/ui/Button.tsx +41 -0
  76. package/src/components/ui/Dropdown.tsx +82 -0
  77. package/src/components/ui/Modal.tsx +135 -0
  78. package/src/components/ui/SlidePanel.tsx +73 -0
  79. package/src/components/ui/Spinner.tsx +24 -0
  80. package/src/components/ui/Toast.tsx +78 -0
  81. package/src/layouts/AdminLayout.astro +197 -0
  82. package/src/lib/config.ts +68 -0
  83. package/src/lib/dataStore.ts +111 -0
  84. package/src/middleware.ts +48 -0
  85. package/src/pages/[collection]/[id].astro +176 -0
  86. package/src/pages/[collection]/index.astro +180 -0
  87. package/src/pages/api/[collection]/[id].ts +258 -0
  88. package/src/pages/api/[collection]/index.ts +289 -0
  89. package/src/pages/api/auth/[id].ts +142 -0
  90. package/src/pages/api/auth/audit-logs.ts +80 -0
  91. package/src/pages/api/auth/login.ts +101 -0
  92. package/src/pages/api/auth/logout.ts +48 -0
  93. package/src/pages/api/auth/me.ts +36 -0
  94. package/src/pages/api/auth/users.ts +150 -0
  95. package/src/pages/audit/index.astro +110 -0
  96. package/src/pages/index.astro +225 -0
  97. package/src/pages/roles/index.astro +114 -0
  98. package/src/pages/users/[id].astro +174 -0
  99. package/src/pages/users/index.astro +142 -0
  100. package/src/pages/users/new.astro +91 -0
  101. package/src/styles/main.css +1449 -0
  102. package/tsconfig.json +12 -0
@@ -0,0 +1,142 @@
1
+ import { A as AstroError, bf as MissingSharp } from './sequence_9cl7AJy-.mjs';
2
+ import { b as baseService, p as parseQuality } from './node_9bvTewss.mjs';
3
+
4
+ let sharp;
5
+ const qualityTable = {
6
+ low: 25,
7
+ mid: 50,
8
+ high: 80,
9
+ max: 100
10
+ };
11
+ function resolveSharpQuality(quality) {
12
+ if (!quality) return void 0;
13
+ const parsedQuality = parseQuality(quality);
14
+ if (typeof parsedQuality === "number") {
15
+ return parsedQuality;
16
+ }
17
+ return quality in qualityTable ? qualityTable[quality] : void 0;
18
+ }
19
+ function resolveSharpEncoderOptions(transform, inputFormat, serviceConfig = {}) {
20
+ const quality = resolveSharpQuality(transform.quality);
21
+ switch (transform.format) {
22
+ case "jpg":
23
+ case "jpeg":
24
+ return {
25
+ ...serviceConfig.jpeg,
26
+ ...quality === void 0 ? {} : { quality }
27
+ };
28
+ case "png":
29
+ return {
30
+ ...serviceConfig.png,
31
+ ...quality === void 0 ? {} : { quality }
32
+ };
33
+ case "webp": {
34
+ const webpOptions = {
35
+ ...serviceConfig.webp,
36
+ ...quality === void 0 ? {} : { quality }
37
+ };
38
+ if (inputFormat === "gif") {
39
+ webpOptions.loop ??= 0;
40
+ }
41
+ return webpOptions;
42
+ }
43
+ case "avif":
44
+ return {
45
+ ...serviceConfig.avif,
46
+ ...quality === void 0 ? {} : { quality }
47
+ };
48
+ default:
49
+ return quality === void 0 ? void 0 : { quality };
50
+ }
51
+ }
52
+ async function loadSharp() {
53
+ let sharpImport;
54
+ try {
55
+ sharpImport = (await import('sharp')).default;
56
+ } catch {
57
+ throw new AstroError(MissingSharp);
58
+ }
59
+ sharpImport.cache(false);
60
+ return sharpImport;
61
+ }
62
+ const fitMap = {
63
+ fill: "fill",
64
+ contain: "inside",
65
+ cover: "cover",
66
+ none: "outside",
67
+ "scale-down": "inside",
68
+ outside: "outside",
69
+ inside: "inside"
70
+ };
71
+ const sharpService = {
72
+ validateOptions: baseService.validateOptions,
73
+ getURL: baseService.getURL,
74
+ parseURL: baseService.parseURL,
75
+ getHTMLAttributes: baseService.getHTMLAttributes,
76
+ getSrcSet: baseService.getSrcSet,
77
+ getRemoteSize: baseService.getRemoteSize,
78
+ async transform(inputBuffer, transformOptions, config) {
79
+ if (!sharp) sharp = await loadSharp();
80
+ const transform = transformOptions;
81
+ const kernel = config.service.config.kernel;
82
+ if (transform.format === "svg") return { data: inputBuffer, format: "svg" };
83
+ const result = sharp(inputBuffer, {
84
+ failOnError: false,
85
+ pages: -1,
86
+ limitInputPixels: config.service.config.limitInputPixels
87
+ });
88
+ result.rotate();
89
+ const { format } = await result.metadata();
90
+ if (transform.width && transform.height) {
91
+ const fit = transform.fit ? fitMap[transform.fit] ?? "inside" : void 0;
92
+ result.resize({
93
+ width: Math.round(transform.width),
94
+ height: Math.round(transform.height),
95
+ kernel,
96
+ fit,
97
+ position: transform.position,
98
+ withoutEnlargement: true
99
+ });
100
+ } else if (transform.height && !transform.width) {
101
+ result.resize({
102
+ height: Math.round(transform.height),
103
+ withoutEnlargement: true,
104
+ kernel
105
+ });
106
+ } else if (transform.width) {
107
+ result.resize({
108
+ width: Math.round(transform.width),
109
+ withoutEnlargement: true,
110
+ kernel
111
+ });
112
+ }
113
+ if (transform.background) {
114
+ result.flatten({ background: transform.background });
115
+ }
116
+ if (transform.format) {
117
+ const encoderOptions = resolveSharpEncoderOptions(transform, format, config.service.config);
118
+ if (transform.format === "webp" && format === "gif") {
119
+ result.webp(encoderOptions);
120
+ } else if (transform.format === "webp") {
121
+ result.webp(encoderOptions);
122
+ } else if (transform.format === "png") {
123
+ result.png(encoderOptions);
124
+ } else if (transform.format === "avif") {
125
+ result.avif(encoderOptions);
126
+ } else if (transform.format === "jpeg" || transform.format === "jpg") {
127
+ result.jpeg(encoderOptions);
128
+ } else {
129
+ result.toFormat(transform.format, encoderOptions);
130
+ }
131
+ }
132
+ const { data, info } = await result.toBuffer({ resolveWithObject: true });
133
+ const needsCopy = "buffer" in data && data.buffer instanceof SharedArrayBuffer;
134
+ return {
135
+ data: needsCopy ? new Uint8Array(data) : data,
136
+ format: info.format
137
+ };
138
+ }
139
+ };
140
+ var sharp_default = sharpService;
141
+
142
+ export { sharp_default as default, resolveSharpEncoderOptions };
@@ -0,0 +1,137 @@
1
+ import { RedisAuthAdapter, createAuditContext, AuditLogger } from './index_CVqOkerS.mjs';
2
+ import bcrypt from 'bcryptjs';
3
+
4
+ const redisAdapter = new RedisAuthAdapter({
5
+ url: process.env.REDIS_URL || "redis://localhost:6379"
6
+ });
7
+ const auditLogger = new AuditLogger(redisAdapter);
8
+ async function ensureConnection() {
9
+ try {
10
+ await redisAdapter.connect();
11
+ } catch (e) {
12
+ }
13
+ }
14
+ const GET = async ({ url, request }) => {
15
+ await ensureConnection();
16
+ const page = parseInt(url.searchParams.get("page") || "1");
17
+ const limit = parseInt(url.searchParams.get("limit") || "25");
18
+ const search = url.searchParams.get("search") || "";
19
+ try {
20
+ const pattern = search ? `*${search.toLowerCase()}*` : "*";
21
+ let cursor = "0";
22
+ const users = [];
23
+ const seenIds = /* @__PURE__ */ new Set();
24
+ do {
25
+ const [nextCursor, keys] = await redisAdapter.redis.scan(
26
+ cursor,
27
+ "MATCH",
28
+ "kyro:auth:users:email:*",
29
+ "COUNT",
30
+ 100
31
+ );
32
+ cursor = nextCursor;
33
+ for (const key of keys) {
34
+ const userId = await redisAdapter.redis.get(key);
35
+ if (userId && !seenIds.has(userId)) {
36
+ seenIds.add(userId);
37
+ const user = await redisAdapter.findUserById(userId);
38
+ if (user) {
39
+ const { passwordHash, ...safeUser } = user;
40
+ users.push(safeUser);
41
+ }
42
+ }
43
+ }
44
+ } while (cursor !== "0");
45
+ const totalDocs = users.length;
46
+ const startIndex = (page - 1) * limit;
47
+ const paginatedUsers = users.slice(startIndex, startIndex + limit);
48
+ return new Response(
49
+ JSON.stringify({
50
+ docs: paginatedUsers,
51
+ totalDocs,
52
+ page,
53
+ limit,
54
+ totalPages: Math.ceil(totalDocs / limit)
55
+ }),
56
+ {
57
+ status: 200,
58
+ headers: { "Content-Type": "application/json" }
59
+ }
60
+ );
61
+ } catch (error) {
62
+ console.error("Error fetching users:", error);
63
+ return new Response(
64
+ JSON.stringify({
65
+ error: "Failed to fetch users",
66
+ docs: [],
67
+ totalDocs: 0
68
+ }),
69
+ {
70
+ status: 200,
71
+ headers: { "Content-Type": "application/json" }
72
+ }
73
+ );
74
+ }
75
+ };
76
+ const POST = async ({ request }) => {
77
+ await ensureConnection();
78
+ const { ipAddress, userAgent } = createAuditContext(request);
79
+ try {
80
+ const body = await request.json();
81
+ const { email, password, role, tenantId } = body;
82
+ if (!email || !password) {
83
+ return new Response(
84
+ JSON.stringify({ error: "Email and password are required" }),
85
+ {
86
+ status: 400,
87
+ headers: { "Content-Type": "application/json" }
88
+ }
89
+ );
90
+ }
91
+ const existing = await redisAdapter.findUserByEmail(email);
92
+ if (existing) {
93
+ return new Response(JSON.stringify({ error: "Email already exists" }), {
94
+ status: 400,
95
+ headers: { "Content-Type": "application/json" }
96
+ });
97
+ }
98
+ const passwordHash = await bcrypt.hash(password, 12);
99
+ const user = await redisAdapter.createUser({
100
+ email,
101
+ passwordHash,
102
+ role: role || "customer",
103
+ tenantId
104
+ });
105
+ await auditLogger.log({
106
+ action: "user_create",
107
+ userId: user.id,
108
+ userEmail: user.email,
109
+ role: user.role,
110
+ resource: "users",
111
+ ipAddress,
112
+ userAgent,
113
+ success: true
114
+ });
115
+ const { passwordHash: _, ...safeUser } = user;
116
+ return new Response(JSON.stringify({ data: safeUser }), {
117
+ status: 201,
118
+ headers: { "Content-Type": "application/json" }
119
+ });
120
+ } catch (error) {
121
+ console.error("Error creating user:", error);
122
+ return new Response(JSON.stringify({ error: "Failed to create user" }), {
123
+ status: 500,
124
+ headers: { "Content-Type": "application/json" }
125
+ });
126
+ }
127
+ };
128
+
129
+ const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
130
+ __proto__: null,
131
+ GET,
132
+ POST
133
+ }, Symbol.toStringTag, { value: 'Module' }));
134
+
135
+ const page = () => _page;
136
+
137
+ export { page };
@@ -0,0 +1,5 @@
1
+ export { h as handler, o as options, b as startServer } from './chunks/server_peBx9VXG.mjs';
2
+ import './chunks/sequence_9cl7AJy-.mjs';
3
+ import 'piccolore';
4
+ import 'es-module-lexer';
5
+ import 'clsx';
@@ -0,0 +1,48 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { ag as sequence } from './chunks/sequence_9cl7AJy-.mjs';
3
+
4
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
5
+ const PUBLIC_PATHS = [
6
+ "/api/auth/login",
7
+ "/api/auth/logout",
8
+ "/api/auth/me",
9
+ "/api/health",
10
+ "/favicon.svg"
11
+ ];
12
+ const PUBLIC_PREFIXES = ["/api/collections/", "/api/auth/"];
13
+ const onRequest$1 = async ({ request, url }, next) => {
14
+ const pathname = new URL(url).pathname;
15
+ if (PUBLIC_PATHS.includes(pathname)) {
16
+ return next();
17
+ }
18
+ for (const prefix of PUBLIC_PREFIXES) {
19
+ if (pathname.startsWith(prefix)) {
20
+ return next();
21
+ }
22
+ }
23
+ const authHeader = request.headers.get("authorization");
24
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
25
+ if (!token) {
26
+ return new Response(JSON.stringify({ error: "Authentication required" }), {
27
+ status: 401,
28
+ headers: { "Content-Type": "application/json" }
29
+ });
30
+ }
31
+ try {
32
+ const payload = jwt.verify(token, JWT_SECRET);
33
+ return next();
34
+ } catch {
35
+ return new Response(JSON.stringify({ error: "Invalid or expired token" }), {
36
+ status: 401,
37
+ headers: { "Content-Type": "application/json" }
38
+ });
39
+ }
40
+ };
41
+
42
+ const onRequest = sequence(
43
+
44
+ onRequest$1
45
+
46
+ );
47
+
48
+ export { onRequest };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@kyro-cms/admin",
3
+ "version": "0.1.2",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Admin dashboard for Kyro CMS",
7
+ "main": "./dist/index.js",
8
+ "scripts": {
9
+ "dev": "astro dev",
10
+ "build": "astro build",
11
+ "preview": "astro preview",
12
+ "check": "astro check"
13
+ },
14
+ "dependencies": {
15
+ "@astrojs/node": "^10.0.4",
16
+ "@astrojs/react": "5.0.2",
17
+ "@kyro-cms/core": "^0.1.2",
18
+ "@tailwindcss/vite": "^4.0.0",
19
+ "astro": "6.1.3",
20
+ "lucide-react": "^0.475.0",
21
+ "react": "^19.0.0",
22
+ "react-dom": "^19.0.0",
23
+ "tailwindcss": "^4.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
28
+ "typescript": "^5.7.0"
29
+ },
30
+ "peerDependencies": {
31
+ "@kyro-cms/core": "^0.1.2"
32
+ }
33
+ }
Binary file
@@ -0,0 +1,155 @@
1
+ import type { CollectionConfig } from "@kyro-cms/core";
2
+
3
+ export const usersCollection: CollectionConfig = {
4
+ slug: "users",
5
+ label: "Users",
6
+ singular: "User",
7
+ type: "singleton",
8
+ fields: [
9
+ { name: "id", type: "text", required: true, readonly: true },
10
+ { name: "email", type: "email", required: true },
11
+ {
12
+ name: "role",
13
+ type: "select",
14
+ options: [
15
+ "super_admin",
16
+ "admin",
17
+ "editor",
18
+ "author",
19
+ "customer",
20
+ "guest",
21
+ ],
22
+ required: true,
23
+ },
24
+ { name: "tenantId", type: "text", label: "Tenant" },
25
+ { name: "emailVerified", type: "boolean", label: "Email Verified" },
26
+ { name: "locked", type: "boolean" },
27
+ {
28
+ name: "lastLogin",
29
+ type: "datetime",
30
+ label: "Last Login",
31
+ readonly: true,
32
+ },
33
+ {
34
+ name: "failedLoginAttempts",
35
+ type: "number",
36
+ label: "Failed Login Attempts",
37
+ readonly: true,
38
+ },
39
+ { name: "createdAt", type: "datetime", readonly: true },
40
+ { name: "updatedAt", type: "datetime", readonly: true },
41
+ ],
42
+ access: {
43
+ create: () => true,
44
+ read: () => true,
45
+ update: () => true,
46
+ delete: () => true,
47
+ },
48
+ list: {
49
+ columns: [
50
+ "email",
51
+ "role",
52
+ "tenantId",
53
+ "emailVerified",
54
+ "locked",
55
+ "lastLogin",
56
+ ],
57
+ defaultSort: "createdAt",
58
+ defaultOrder: "desc",
59
+ },
60
+ };
61
+
62
+ export const rolesCollection: CollectionConfig = {
63
+ slug: "roles",
64
+ label: "Roles",
65
+ singular: "Role",
66
+ type: "collection",
67
+ fields: [
68
+ { name: "name", type: "text", required: true },
69
+ { name: "level", type: "number", required: true },
70
+ {
71
+ name: "inherits",
72
+ type: "array",
73
+ items: { type: "text" },
74
+ label: "Inherits From",
75
+ },
76
+ { name: "description", type: "textarea" },
77
+ {
78
+ name: "permissions",
79
+ type: "array",
80
+ items: { type: "text" },
81
+ label: "Permissions",
82
+ },
83
+ ],
84
+ access: {
85
+ create: () => true,
86
+ read: () => true,
87
+ update: () => true,
88
+ delete: () => true,
89
+ },
90
+ list: {
91
+ columns: ["name", "level", "inherits", "description"],
92
+ defaultSort: "level",
93
+ defaultOrder: "desc",
94
+ },
95
+ };
96
+
97
+ export interface AuditLogEntry {
98
+ id: string;
99
+ action: string;
100
+ userId?: string;
101
+ userEmail?: string;
102
+ role?: string;
103
+ resource: string;
104
+ ipAddress?: string;
105
+ userAgent?: string;
106
+ success: boolean;
107
+ error?: string;
108
+ metadata?: Record<string, unknown>;
109
+ timestamp: string;
110
+ }
111
+
112
+ export const auditLogsCollection: CollectionConfig = {
113
+ slug: "audit_logs",
114
+ label: "Audit Logs",
115
+ singular: "Audit Log",
116
+ type: "collection",
117
+ fields: [
118
+ { name: "id", type: "text", required: true, readonly: true },
119
+ { name: "action", type: "text", required: true },
120
+ { name: "userId", type: "text", label: "User ID" },
121
+ { name: "userEmail", type: "email", label: "User Email" },
122
+ { name: "role", type: "text" },
123
+ { name: "resource", type: "text", label: "Resource" },
124
+ { name: "ipAddress", type: "text", label: "IP Address" },
125
+ { name: "userAgent", type: "text", label: "User Agent" },
126
+ { name: "success", type: "boolean" },
127
+ { name: "error", type: "textarea" },
128
+ { name: "metadata", type: "json", label: "Metadata" },
129
+ { name: "timestamp", type: "datetime", required: true, readonly: true },
130
+ ],
131
+ access: {
132
+ create: () => false,
133
+ read: () => true,
134
+ update: () => false,
135
+ delete: () => false,
136
+ },
137
+ list: {
138
+ columns: [
139
+ "action",
140
+ "userEmail",
141
+ "role",
142
+ "resource",
143
+ "success",
144
+ "timestamp",
145
+ ],
146
+ defaultSort: "timestamp",
147
+ defaultOrder: "desc",
148
+ },
149
+ };
150
+
151
+ export const authCollections = {
152
+ users: usersCollection,
153
+ roles: rolesCollection,
154
+ audit_logs: auditLogsCollection,
155
+ };