@arch-cadre/panel 1.0.6 → 1.0.9

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 (108) hide show
  1. package/dist/actions/activity-log/index.cjs +1 -1
  2. package/dist/actions/activity-log/index.mjs +1 -1
  3. package/dist/actions/index.cjs +2 -2
  4. package/dist/actions/index.d.ts +2 -2
  5. package/dist/actions/index.mjs +2 -2
  6. package/dist/actions/profile.d.ts +1 -1
  7. package/dist/index.cjs +7 -7
  8. package/dist/index.mjs +7 -7
  9. package/dist/routes.cjs +10 -10
  10. package/dist/routes.mjs +10 -10
  11. package/dist/schema.cjs +1 -1
  12. package/dist/schema.d.ts +1 -1
  13. package/dist/schema.mjs +1 -1
  14. package/dist/ui/activity-log/components/ActivityStatsWidget.cjs +1 -1
  15. package/dist/ui/activity-log/components/ActivityStatsWidget.mjs +1 -1
  16. package/dist/ui/activity-log/components/RecentLogsWidget.cjs +1 -1
  17. package/dist/ui/activity-log/components/RecentLogsWidget.mjs +1 -1
  18. package/dist/ui/activity-log/pages/log-list.cjs +2 -2
  19. package/dist/ui/activity-log/pages/log-list.mjs +1 -1
  20. package/dist/ui/components/app-content.cjs +1 -1
  21. package/dist/ui/components/app-content.mjs +1 -1
  22. package/dist/ui/components/app-sidebar.cjs +3 -3
  23. package/dist/ui/components/app-sidebar.mjs +2 -2
  24. package/dist/ui/components/app-user.cjs +5 -5
  25. package/dist/ui/components/app-user.mjs +4 -4
  26. package/dist/ui/components/breadcrumb-slot.cjs +2 -2
  27. package/dist/ui/components/breadcrumb-slot.mjs +1 -1
  28. package/dist/ui/components/manager/module-card.cjs +3 -3
  29. package/dist/ui/components/manager/module-card.mjs +2 -2
  30. package/dist/ui/components/manager/module-list.cjs +3 -3
  31. package/dist/ui/components/manager/module-list.mjs +2 -2
  32. package/dist/ui/components/manager/module-upload.cjs +3 -3
  33. package/dist/ui/components/manager/module-upload.mjs +2 -2
  34. package/dist/ui/components/profile/components.cjs +7 -7
  35. package/dist/ui/components/profile/components.d.ts +1 -1
  36. package/dist/ui/components/profile/components.mjs +3 -3
  37. package/dist/ui/components/profile/link.cjs +2 -2
  38. package/dist/ui/components/profile/link.mjs +1 -1
  39. package/dist/ui/components/profile/page.cjs +2 -6
  40. package/dist/ui/components/profile/page.mjs +3 -10
  41. package/dist/ui/components/sidebar-slot.cjs +1 -1
  42. package/dist/ui/components/sidebar-slot.mjs +1 -1
  43. package/dist/ui/dashboard/page.cjs +3 -3
  44. package/dist/ui/dashboard/page.mjs +1 -1
  45. package/dist/ui/dashboard/widgets/WelcomeBackUserWidget.cjs +1 -0
  46. package/dist/ui/dashboard/widgets/WelcomeBackUserWidget.mjs +5 -4
  47. package/dist/ui/error.cjs +2 -2
  48. package/dist/ui/error.mjs +1 -1
  49. package/dist/ui/layout.cjs +3 -3
  50. package/dist/ui/layout.mjs +3 -3
  51. package/dist/ui/modules/docs/page.cjs +1 -1
  52. package/dist/ui/modules/docs/page.mjs +1 -1
  53. package/dist/ui/modules/page.cjs +3 -3
  54. package/dist/ui/modules/page.mjs +3 -3
  55. package/dist/ui/rbac/pages/rbac-admin.cjs +16 -16
  56. package/dist/ui/rbac/pages/rbac-admin.mjs +2 -2
  57. package/dist/ui/router.cjs +1 -1
  58. package/dist/ui/router.mjs +1 -1
  59. package/dist/ui/session-manager/components/sessions-list.cjs +7 -7
  60. package/dist/ui/session-manager/components/sessions-list.mjs +3 -3
  61. package/dist/ui/session-manager/pages/sessions-page.cjs +4 -4
  62. package/dist/ui/session-manager/pages/sessions-page.mjs +3 -3
  63. package/dist/ui/settings-page.cjs +3 -3
  64. package/dist/ui/settings-page.mjs +2 -2
  65. package/package.json +7 -6
  66. package/src/actions/actions.ts +17 -0
  67. package/src/actions/activity-log/index.ts +17 -0
  68. package/src/actions/index.ts +2 -0
  69. package/src/actions/manager.ts +168 -0
  70. package/src/actions/profile.ts +173 -0
  71. package/src/actions/rbac/index.ts +131 -0
  72. package/src/actions/session-manager/index.ts +87 -0
  73. package/src/actions/settings.ts +34 -0
  74. package/src/index.ts +135 -0
  75. package/src/intl.d.ts +9 -0
  76. package/src/navigation.ts +57 -0
  77. package/src/routes.ts +107 -0
  78. package/src/schema/activity-log.ts +16 -0
  79. package/src/schema.ts +1 -0
  80. package/src/types.ts +18 -0
  81. package/src/ui/activity-log/components/ActivityStatsWidget.tsx +37 -0
  82. package/src/ui/activity-log/components/RecentLogsWidget.tsx +74 -0
  83. package/src/ui/activity-log/pages/log-list.tsx +91 -0
  84. package/src/ui/components/app-content.tsx +51 -0
  85. package/src/ui/components/app-header.tsx +65 -0
  86. package/src/ui/components/app-sidebar.tsx +249 -0
  87. package/src/ui/components/app-user.tsx +126 -0
  88. package/src/ui/components/breadcrumb-slot.tsx +52 -0
  89. package/src/ui/components/manager/module-card.tsx +327 -0
  90. package/src/ui/components/manager/module-list.tsx +59 -0
  91. package/src/ui/components/manager/module-upload.tsx +84 -0
  92. package/src/ui/components/profile/components.tsx +311 -0
  93. package/src/ui/components/profile/link.tsx +36 -0
  94. package/src/ui/components/profile/page.tsx +45 -0
  95. package/src/ui/components/sidebar-slot.tsx +47 -0
  96. package/src/ui/dashboard/page.tsx +17 -0
  97. package/src/ui/dashboard/widgets/WelcomeBackUserWidget.tsx +47 -0
  98. package/src/ui/error.tsx +82 -0
  99. package/src/ui/layout.tsx +54 -0
  100. package/src/ui/modules/docs/page.tsx +105 -0
  101. package/src/ui/modules/page.tsx +30 -0
  102. package/src/ui/page.tsx +15 -0
  103. package/src/ui/rbac/pages/rbac-admin.tsx +551 -0
  104. package/src/ui/router.tsx +69 -0
  105. package/src/ui/session-manager/components/sessions-list.tsx +303 -0
  106. package/src/ui/session-manager/pages/sessions-page.tsx +22 -0
  107. package/src/ui/settings/page.tsx +73 -0
  108. package/src/ui/settings-page.tsx +97 -0
@@ -0,0 +1,17 @@
1
+ "use server";
2
+
3
+ import { signOut as signOutAction } from "@arch-cadre/core/server";
4
+ import { getTranslation } from "@arch-cadre/intl/server";
5
+ import { revalidatePath } from "next/cache";
6
+
7
+ /**
8
+ * Modern Logout Action delegating to Core Auth
9
+ */
10
+ export async function logoutAction() {
11
+ await signOutAction();
12
+ const { t } = await getTranslation();
13
+
14
+ t("Pos");
15
+
16
+ revalidatePath("/", "layout");
17
+ }
@@ -0,0 +1,17 @@
1
+ "use server";
2
+
3
+ import { db, userTable } from "@arch-cadre/core/server";
4
+ import { desc, eq } from "drizzle-orm";
5
+ import { activityLogsTable } from "../../schema/activity-log.js";
6
+
7
+ export async function getActivityLogs() {
8
+ return await db
9
+ .select({
10
+ log: activityLogsTable,
11
+ user: { name: userTable.name, email: userTable.email },
12
+ })
13
+ .from(activityLogsTable)
14
+ .leftJoin(userTable, eq(activityLogsTable.userId, userTable.id))
15
+ .orderBy(desc(activityLogsTable.createdAt))
16
+ .limit(100);
17
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./actions.js";
2
+ export * from "./settings.js";
@@ -0,0 +1,168 @@
1
+ "use server";
2
+
3
+ import { exec } from "node:child_process";
4
+ import fs, { constants } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ import {
8
+ db,
9
+ eventBus,
10
+ getModulesDir,
11
+ systemModulesTable,
12
+ } from "@arch-cadre/core/server";
13
+ import {
14
+ getModuleStatus,
15
+ getModules,
16
+ ModuleManifestSchema,
17
+ toggleModuleState,
18
+ } from "@arch-cadre/modules/server";
19
+ import AdmZip from "adm-zip";
20
+ import { revalidatePath, unstable_noStore } from "next/cache";
21
+
22
+ export async function toggleModuleAction(moduleId: string, isEnabled: boolean) {
23
+ try {
24
+ console.log(
25
+ `[Module:ModuleManager] Toggle request for ${moduleId} -> ${isEnabled}`,
26
+ );
27
+ const result = await toggleModuleState(moduleId, isEnabled);
28
+ await eventBus.publish("system:module:toggle", { moduleId, isEnabled });
29
+ revalidatePath("/admin/modules");
30
+ revalidatePath("/module/module-manager");
31
+ return result;
32
+ } catch (error) {
33
+ console.error(
34
+ `[Module:ModuleManager] Error toggling module ${moduleId}:`,
35
+ error,
36
+ );
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ export async function uploadModuleAction(formData: FormData) {
42
+ try {
43
+ const file = formData.get("file") as File;
44
+ if (!file) throw new Error("No file provided");
45
+
46
+ const buffer = Buffer.from(await file.arrayBuffer());
47
+ const zip = new AdmZip(buffer);
48
+ const zipEntries = zip.getEntries();
49
+
50
+ const manifestEntry = zipEntries.find((e) =>
51
+ e.entryName.endsWith("manifest.json"),
52
+ );
53
+ if (!manifestEntry) throw new Error("ZIP must contain manifest.json");
54
+
55
+ const moduleRootInZip = manifestEntry.entryName.replace(
56
+ "manifest.json",
57
+ "",
58
+ );
59
+
60
+ const manifestRaw = JSON.parse(manifestEntry.getData().toString("utf8"));
61
+ const manifest = ModuleManifestSchema.parse(manifestRaw);
62
+
63
+ const modulesDir = await getModulesDir();
64
+ const targetDir = path.join(modulesDir, manifest.id);
65
+
66
+ try {
67
+ await fs.access(targetDir);
68
+ throw new Error(`Module with ID "${manifest.id}" already exists.`);
69
+ } catch (e: any) {
70
+ if (e.message.includes("already exists")) throw e;
71
+ }
72
+
73
+ await fs.mkdir(targetDir, { recursive: true });
74
+
75
+ for (const entry of zipEntries) {
76
+ if (entry.entryName.startsWith(moduleRootInZip) && !entry.isDirectory) {
77
+ const relativePath = entry.entryName.slice(moduleRootInZip.length);
78
+ const fullPath = path.join(targetDir, relativePath);
79
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
80
+ await fs.writeFile(fullPath, entry.getData());
81
+ }
82
+ }
83
+
84
+ await db
85
+ .insert(systemModulesTable)
86
+ .values({
87
+ id: manifest.id,
88
+ enabled: false,
89
+ installed: false,
90
+ system: manifest.system ?? false,
91
+ })
92
+ .onConflictDoNothing();
93
+
94
+ console.log(
95
+ `[Module:ModuleManager] Module "${manifest.id}" uploaded and extracted.`,
96
+ );
97
+ await eventBus.publish("system:module:upload", {
98
+ moduleId: manifest.id,
99
+ manifest,
100
+ });
101
+ revalidatePath("/admin/modules");
102
+ revalidatePath("/module/module-manager");
103
+ return { success: true };
104
+ } catch (error) {
105
+ console.error(`[Module:ModuleManager] Upload error:`, error);
106
+ return { success: false, error: (error as Error).message };
107
+ }
108
+ }
109
+
110
+ export async function checkDiskWriteAccess() {
111
+ const modulesDir = await getModulesDir();
112
+ try {
113
+ await fs.access(modulesDir, constants.W_OK);
114
+ return { canWrite: true };
115
+ } catch (error) {
116
+ console.error(
117
+ "[Module:ModuleManager] No write access to modules directory:",
118
+ error,
119
+ );
120
+ return { canWrite: false };
121
+ }
122
+ }
123
+
124
+ export async function getModuleStatusAction(moduleId: string) {
125
+ unstable_noStore();
126
+ return await getModuleStatus(moduleId);
127
+ }
128
+
129
+ export async function getModulesAction() {
130
+ unstable_noStore();
131
+ return await getModules();
132
+ }
133
+
134
+ export async function getModuleDocumentation(moduleId: string) {
135
+ const modulesDir = await getModulesDir();
136
+ const docsDir = path.join(modulesDir, moduleId, "docs");
137
+ try {
138
+ const exists = await fs
139
+ .access(docsDir)
140
+ .then(() => true)
141
+ .catch(() => false);
142
+ if (!exists) return null;
143
+
144
+ const files = await fs.readdir(docsDir);
145
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
146
+
147
+ if (mdFiles.length === 0) return null;
148
+
149
+ const docs = await Promise.all(
150
+ mdFiles.map(async (file) => {
151
+ const content = await fs.readFile(path.join(docsDir, file), "utf-8");
152
+ return {
153
+ filename: file,
154
+ title: file
155
+ .replace(".md", "")
156
+ .split("-")
157
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
158
+ .join(" "),
159
+ content,
160
+ };
161
+ }),
162
+ );
163
+
164
+ return docs;
165
+ } catch (_error) {
166
+ return null;
167
+ }
168
+ }
@@ -0,0 +1,173 @@
1
+ "use server";
2
+
3
+ import type { SessionFlags } from "@arch-cadre/core";
4
+ import {
5
+ checkEmailAvailability,
6
+ createEmailVerificationRequest,
7
+ createSession,
8
+ eventBus,
9
+ filesystemService,
10
+ generateSessionToken,
11
+ getCurrentSession,
12
+ getUserPasswordHash,
13
+ invalidateUserSessions,
14
+ sendVerificationEmail,
15
+ setEmailVerificationRequestCookie,
16
+ setSessionTokenCookie,
17
+ updateUserAwatar,
18
+ updateUserName,
19
+ updateUserPassword,
20
+ verifyEmailInput,
21
+ verifyPasswordHash,
22
+ verifyPasswordStrength,
23
+ } from "@arch-cadre/core/server";
24
+ import { getTranslation } from "@arch-cadre/intl/server";
25
+ import { revalidatePath } from "next/cache";
26
+ import { redirect } from "next/navigation";
27
+ import type { ActionResult } from "../types.js";
28
+
29
+ export async function updatePasswordAction(
30
+ _prev: ActionResult,
31
+ formData: FormData,
32
+ ): Promise<ActionResult> {
33
+ const { t } = await getTranslation();
34
+ const { session, user } = await getCurrentSession();
35
+ if (session === null || user === null) {
36
+ return { success: false, message: t("Not authenticated") };
37
+ }
38
+
39
+ if (user.registered2FA && !session.two_factor_verified) {
40
+ return { success: false, message: t("Forbidden") };
41
+ }
42
+
43
+ const password = formData.get("password");
44
+ const newPassword = formData.get("new_password");
45
+
46
+ if (typeof password !== "string" || typeof newPassword !== "string") {
47
+ return { success: false, message: t("Invalid or missing fields") };
48
+ }
49
+
50
+ if (!verifyPasswordStrength(newPassword)) {
51
+ return { success: false, message: t("Weak password") };
52
+ }
53
+
54
+ const passwordHash = await getUserPasswordHash(user.id);
55
+ if (!passwordHash) return { success: false, message: t("Internal error") };
56
+
57
+ const validPassword = await verifyPasswordHash(passwordHash, password);
58
+ if (!validPassword) {
59
+ return { success: false, message: t("Incorrect password") };
60
+ }
61
+
62
+ await invalidateUserSessions(user.id);
63
+ await updateUserPassword(user.id, newPassword);
64
+
65
+ const sessionToken = await generateSessionToken();
66
+ const sessionFlags: SessionFlags = {
67
+ twoFactorVerified: session.two_factor_verified ?? false,
68
+ };
69
+ const newSession = await createSession(sessionToken, user.id, sessionFlags);
70
+ await setSessionTokenCookie(sessionToken, newSession.expiresAt);
71
+
72
+ await eventBus.publish(
73
+ "activity.create",
74
+ {
75
+ action: "profile.updated.password",
76
+ description: `User ${user?.name} updated their password`,
77
+ userId: user?.id,
78
+ metadata: {},
79
+ },
80
+ "profile",
81
+ );
82
+
83
+ return { success: true, message: t("Updated password") };
84
+ }
85
+
86
+ export async function updateEmailAction(
87
+ _prev: ActionResult,
88
+ formData: FormData,
89
+ ): Promise<ActionResult> {
90
+ const { t } = await getTranslation();
91
+ const { session, user } = await getCurrentSession();
92
+ if (session === null || user === null)
93
+ return { success: false, message: t("Not authenticated") };
94
+ if (user.registered2FA && !session.two_factor_verified)
95
+ return { success: false, message: t("Forbidden") };
96
+
97
+ const email = formData.get("email");
98
+ if (typeof email !== "string" || email === "") {
99
+ return { success: false, message: t("Please enter your email") };
100
+ }
101
+ if (!verifyEmailInput(email)) {
102
+ return { success: false, message: t("Please enter a valid email") };
103
+ }
104
+ const emailAvailable = await checkEmailAvailability(email);
105
+ if (!emailAvailable) {
106
+ return { success: false, message: t("This email is already used") };
107
+ }
108
+
109
+ const verificationRequest = await createEmailVerificationRequest(
110
+ user.id,
111
+ email,
112
+ );
113
+ await sendVerificationEmail(
114
+ verificationRequest.email,
115
+ verificationRequest.code,
116
+ );
117
+ await setEmailVerificationRequestCookie(verificationRequest);
118
+
119
+ await eventBus.publish(
120
+ "activity.create",
121
+ {
122
+ action: "profile.updated.email",
123
+ description: `User ${user?.name} updated their email`,
124
+ userId: user?.id,
125
+ metadata: { email },
126
+ },
127
+ "profile",
128
+ );
129
+
130
+ return redirect("/verify-email");
131
+ }
132
+
133
+ export async function updateProfileAction(
134
+ name: string,
135
+ image?: File,
136
+ ): Promise<{ error?: string; success?: boolean; message?: string }> {
137
+ const { session, user } = await getCurrentSession();
138
+
139
+ const { t } = await getTranslation();
140
+
141
+ if (session === null || user === null)
142
+ return { error: t("Not authenticated") };
143
+
144
+ if (!name || name.trim().length === 0)
145
+ return { error: t("Name is required") };
146
+ if (name.trim().length > 100) return { error: t("Name is too long") };
147
+
148
+ if (image instanceof File) {
149
+ const uploaded = await filesystemService.upload(image, "vercel-blob");
150
+
151
+ if ("error" in uploaded) {
152
+ return { error: uploaded.error };
153
+ }
154
+
155
+ await updateUserAwatar(user.id, uploaded.url);
156
+ }
157
+
158
+ await updateUserName(user.id, name.trim());
159
+ revalidatePath("/profile"); // Updated path
160
+
161
+ await eventBus.publish(
162
+ "activity.create",
163
+ {
164
+ action: "profile.updated",
165
+ description: `User ${user?.name} updated their profile`,
166
+ userId: user?.id,
167
+ metadata: { name, imageUrl: image instanceof File ? image.name : null },
168
+ },
169
+ "profile",
170
+ );
171
+
172
+ return { success: true, message: t("Profile updated successfully") };
173
+ }
@@ -0,0 +1,131 @@
1
+ "use server";
2
+
3
+ import * as rbac from "@arch-cadre/core/server";
4
+ import { db, eventBus, userTable } from "@arch-cadre/core/server";
5
+ import { revalidatePath } from "next/cache";
6
+
7
+ /**
8
+ * RBAC Manager Module Actions
9
+ * These actions act as a bridge between the UI and the Core RBAC Logic.
10
+ */
11
+
12
+ export async function getRoles() {
13
+ return await rbac.getRoles();
14
+ }
15
+
16
+ export async function createRole(name: string, description?: string) {
17
+ const result = await rbac.createRole(name, description);
18
+ revalidatePath("/", "layout");
19
+ return JSON.parse(JSON.stringify(result));
20
+ }
21
+
22
+ export async function deleteRole(roleId: string) {
23
+ await rbac.deleteRole(roleId);
24
+ revalidatePath("/", "layout");
25
+ return { success: true };
26
+ }
27
+
28
+ export async function getPermissions() {
29
+ return await rbac.getPermissions();
30
+ }
31
+
32
+ export async function createPermission(name: string, description?: string) {
33
+ const result = await rbac.createPermission(name, description);
34
+ revalidatePath("/", "layout");
35
+ return JSON.parse(JSON.stringify(result));
36
+ }
37
+
38
+ export async function deletePermission(permissionId: string) {
39
+ await rbac.deletePermission(permissionId);
40
+ revalidatePath("/", "layout");
41
+ return { success: true };
42
+ }
43
+
44
+ export async function getRolePermissions(roleId: string) {
45
+ return await rbac.getRolePermissions(roleId);
46
+ }
47
+
48
+ export async function assignPermissionToRole(
49
+ roleId: string,
50
+ permissionId: string,
51
+ ) {
52
+ await rbac.assignPermissionToRole(roleId, permissionId);
53
+ revalidatePath("/", "layout");
54
+ return { success: true };
55
+ }
56
+
57
+ export async function revokePermissionFromRole(
58
+ roleId: string,
59
+ permissionId: string,
60
+ ) {
61
+ await rbac.revokePermissionFromRole(roleId, permissionId);
62
+ revalidatePath("/", "layout");
63
+ return { success: true };
64
+ }
65
+
66
+ // User-specific actions (could also be in a 'user-manager' module)
67
+ export async function getUsers() {
68
+ return await db
69
+ .select({
70
+ id: userTable.id,
71
+ name: userTable.name,
72
+ email: userTable.email,
73
+ image: userTable.image,
74
+ })
75
+ .from(userTable);
76
+ }
77
+ export async function getUserRbacData(userId: string) {
78
+ return await rbac.getUserRbacData(userId);
79
+ }
80
+
81
+ export async function assignRoleToUser(userId: string, roleId: string) {
82
+ const role = await rbac.getRoleById(roleId);
83
+ await rbac.assignRoleToUser(userId, roleId);
84
+
85
+ if (role) {
86
+ await eventBus.publish("notification:send", {
87
+ userId,
88
+ title: "Role assigned",
89
+ content: `You have been assigned the role: ${role.name}`,
90
+ type: "info",
91
+ });
92
+ }
93
+
94
+ revalidatePath("/", "layout");
95
+ return { success: true };
96
+ }
97
+
98
+ export async function revokeRoleFromUser(userId: string, roleId: string) {
99
+ const role = await rbac.getRoleById(roleId);
100
+ await rbac.revokeRoleFromUser(userId, roleId);
101
+
102
+ if (role) {
103
+ await eventBus.publish("notification:send", {
104
+ userId,
105
+ title: "Role revoked",
106
+ content: `The role ${role.name} has been removed from your account`,
107
+ type: "warning",
108
+ });
109
+ }
110
+
111
+ revalidatePath("/", "layout");
112
+ return { success: true };
113
+ }
114
+
115
+ export async function assignPermissionToUser(
116
+ userId: string,
117
+ permissionId: string,
118
+ ) {
119
+ await rbac.assignPermissionToUser(userId, permissionId);
120
+ revalidatePath("/", "layout");
121
+ return { success: true };
122
+ }
123
+
124
+ export async function revokePermissionFromUser(
125
+ userId: string,
126
+ permissionId: string,
127
+ ) {
128
+ await rbac.revokePermissionFromUser(userId, permissionId);
129
+ revalidatePath("/", "layout");
130
+ return { success: true };
131
+ }
@@ -0,0 +1,87 @@
1
+ "use server";
2
+
3
+ import {
4
+ eventBus,
5
+ getCurrentSession,
6
+ getUserSessions,
7
+ invalidateOtherSessions,
8
+ invalidateSession,
9
+ } from "@arch-cadre/core/server";
10
+ import { getTranslation } from "@arch-cadre/intl/server";
11
+ import { revalidatePath } from "next/cache";
12
+
13
+ export async function getSessionsAction() {
14
+ const { t } = await getTranslation();
15
+ const { session, user } = await getCurrentSession();
16
+ if (!session || !user) return { error: t("Not authenticated"), sessions: [] };
17
+
18
+ const sessions = await getUserSessions(user.id, session.id);
19
+ const sortedSessions = sessions.sort((a, b) => {
20
+ if (a.isCurrent) return -1;
21
+ if (b.isCurrent) return 1;
22
+ return b.createdAt.getTime() - a.createdAt.getTime();
23
+ });
24
+
25
+ return {
26
+ sessions: sortedSessions.map((s) => ({
27
+ id: s.id,
28
+ createdAt: s.createdAt,
29
+ expiresAt: s.expiresAt,
30
+ twoFactorVerified: s.twoFactorVerified,
31
+ isCurrent: s.isCurrent,
32
+ })),
33
+ };
34
+ }
35
+
36
+ export async function revokeSessionAction(sessionId: string) {
37
+ const { t } = await getTranslation();
38
+ const { session, user } = await getCurrentSession();
39
+ if (!session || !user) return { error: t("Not authenticated") };
40
+
41
+ if (sessionId === session.id) {
42
+ return { error: t("Cannot revoke current session. Use sign out instead.") };
43
+ }
44
+
45
+ const userSessions = await getUserSessions(user.id, session.id);
46
+ if (!userSessions.find((s) => s.id === sessionId)) {
47
+ return { error: t("Session not found") };
48
+ }
49
+
50
+ await invalidateSession(sessionId);
51
+ revalidatePath("/settings/sessions");
52
+
53
+ await eventBus.publish(
54
+ "activity.create",
55
+ {
56
+ action: "session.revoked",
57
+ description: `User ${user?.name} revoked a session`,
58
+ userId: user?.id,
59
+ metadata: { sessionId },
60
+ },
61
+ "session-manager",
62
+ );
63
+
64
+ return { success: true };
65
+ }
66
+
67
+ export async function revokeAllOtherSessionsAction() {
68
+ const { session, user } = await getCurrentSession();
69
+ const { t } = await getTranslation();
70
+ if (!session || !user) return { error: t("Not authenticated") };
71
+
72
+ await invalidateOtherSessions(user.id, session.id);
73
+ revalidatePath("/settings/sessions");
74
+
75
+ await eventBus.publish(
76
+ "activity.create",
77
+ {
78
+ action: "session.revoked.all-others",
79
+ description: `User ${user?.name} revoked all other sessions`,
80
+ userId: user?.id,
81
+ metadata: {},
82
+ },
83
+ "session-manager",
84
+ );
85
+
86
+ return { success: true };
87
+ }
@@ -0,0 +1,34 @@
1
+ "use server";
2
+
3
+ import {
4
+ getModuleConfig,
5
+ updateModuleConfig,
6
+ } from "@arch-cadre/modules/server";
7
+ import { revalidatePath } from "next/cache";
8
+
9
+ export interface KryoPanelConfig {
10
+ pathPrefix: string;
11
+ }
12
+
13
+ export async function getKryoConfig(): Promise<KryoPanelConfig> {
14
+ const config = await getModuleConfig<KryoPanelConfig>("kryo-panel");
15
+ return (
16
+ config ?? {
17
+ pathPrefix: "/kryo",
18
+ }
19
+ );
20
+ }
21
+
22
+ export async function updateKryoConfig(config: KryoPanelConfig) {
23
+ // Simple validation: must start with / and be at least 2 chars
24
+ if (!config.pathPrefix.startsWith("/") || config.pathPrefix.length < 2) {
25
+ throw new Error("Path prefix must start with '/' and be valid.");
26
+ }
27
+
28
+ await updateModuleConfig("kryo-panel", config);
29
+
30
+ // Revalidate everything since the base path changed
31
+ revalidatePath("/", "layout");
32
+
33
+ return { success: true };
34
+ }