@kyro-cms/admin 0.3.1 → 0.3.4
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/dist/EditorClient-XEUOVAAC.js +466 -0
- package/dist/EditorClient-XEUOVAAC.js.map +1 -0
- package/dist/EditorClient-YLCGVDXY.cjs +468 -0
- package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
- package/dist/chunk-7KPIUCGT.js +384 -0
- package/dist/chunk-7KPIUCGT.js.map +1 -0
- package/dist/chunk-GOACG6R7.cjs +473 -0
- package/dist/chunk-GOACG6R7.cjs.map +1 -0
- package/dist/index.cjs +14861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +1661 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +563 -0
- package/dist/index.js +14784 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -19
- package/src/components/ActionBar.tsx +7 -43
- package/src/components/Admin.tsx +138 -277
- package/src/components/ApiKeysManager.tsx +428 -419
- package/src/components/AuditLogsPage.tsx +35 -39
- package/src/components/AuthBridge.tsx +51 -0
- package/src/components/AutoForm.tsx +495 -1230
- package/src/components/BrandingHub.tsx +18 -19
- package/src/components/BulkActionsBar.tsx +1 -1
- package/src/components/CreateView.tsx +22 -36
- package/src/components/Dashboard.tsx +60 -84
- package/src/components/DetailView.tsx +113 -91
- package/src/components/DeveloperCenter.tsx +200 -198
- package/src/components/FieldRenderer.tsx +206 -0
- package/src/components/GraphQLPlayground.tsx +340 -480
- package/src/components/ListView.tsx +828 -254
- package/src/components/LoginPage.tsx +3 -4
- package/src/components/MarketplaceManager.tsx +254 -0
- package/src/components/MediaGallery.tsx +856 -1192
- package/src/components/PluginsManager.tsx +277 -0
- package/src/components/RestPlayground.tsx +398 -560
- package/src/components/SessionsManager.tsx +211 -0
- package/src/components/Sidebar.astro +179 -151
- package/src/components/ThemeProvider.tsx +7 -161
- package/src/components/UserManagement.tsx +162 -146
- package/src/components/UserMenu.tsx +110 -0
- package/src/components/WebhookManager.tsx +305 -367
- package/src/components/blocks/AccordionBlock.tsx +4 -4
- package/src/components/blocks/ArrayBlock.tsx +3 -3
- package/src/components/blocks/BlockEditModal.tsx +8 -8
- package/src/components/blocks/BlockWrapper.tsx +61 -0
- package/src/components/blocks/ButtonBlock.tsx +4 -4
- package/src/components/blocks/ChildBlocksTree.tsx +23 -25
- package/src/components/blocks/CodeBlock.tsx +15 -15
- package/src/components/blocks/ColumnsBlock.tsx +6 -44
- package/src/components/blocks/DividerBlock.tsx +3 -3
- package/src/components/blocks/FileBlock.tsx +4 -4
- package/src/components/blocks/HeadingBlock.tsx +6 -38
- package/src/components/blocks/HeroBlock.tsx +4 -4
- package/src/components/blocks/ImageBlock.tsx +4 -4
- package/src/components/blocks/LinkBlock.tsx +4 -4
- package/src/components/blocks/ListBlock.tsx +3 -3
- package/src/components/blocks/ParagraphBlock.tsx +12 -42
- package/src/components/blocks/RelationshipBlock.tsx +4 -4
- package/src/components/blocks/RichTextBlock.tsx +4 -4
- package/src/components/blocks/VStackBlock.tsx +5 -37
- package/src/components/blocks/VideoBlock.tsx +4 -4
- package/src/components/blocks/types.ts +11 -0
- package/src/components/fields/AccordionField.tsx +1 -1
- package/src/components/fields/ArrayField.tsx +2 -2
- package/src/components/fields/ArrayLayout.tsx +93 -0
- package/src/components/fields/BlocksField.tsx +122 -111
- package/src/components/fields/ButtonField.tsx +1 -1
- package/src/components/fields/CheckboxField.tsx +14 -15
- package/src/components/fields/ChildrenField.tsx +2 -2
- package/src/components/fields/CodeField.tsx +3 -3
- package/src/components/fields/ColumnsField.tsx +2 -2
- package/src/components/fields/DateField.tsx +13 -26
- package/src/components/fields/EditorClient.tsx +26 -28
- package/src/components/fields/FieldLayout.tsx +52 -0
- package/src/components/fields/GroupLayout.tsx +35 -0
- package/src/components/fields/JSONField.tsx +7 -7
- package/src/components/fields/LinkField.tsx +1 -1
- package/src/components/fields/MarkdownField.tsx +1 -1
- package/src/components/fields/NumberField.tsx +13 -26
- package/src/components/fields/PortableTextField.tsx +4 -4
- package/src/components/fields/PortableTextRenderer.tsx +1 -1
- package/src/components/fields/RelationshipBlockField.tsx +31 -23
- package/src/components/fields/RelationshipField.tsx +14 -14
- package/src/components/fields/SelectField.tsx +17 -26
- package/src/components/fields/TabsLayout.tsx +69 -0
- package/src/components/fields/TextField.tsx +85 -38
- package/src/components/fields/UploadField.tsx +71 -41
- package/src/components/fields/VideoField.tsx +1 -1
- package/src/components/fields/extensions/blockComponents.tsx +2 -2
- package/src/components/fields/extensions/blocksStore.ts +207 -193
- package/src/components/fields/types.ts +22 -0
- package/src/components/layout/Layout.tsx +1 -1
- package/src/components/ui/ActionMenu.tsx +63 -0
- package/src/components/ui/Badge.tsx +59 -5
- package/src/components/ui/BlockDrawer.tsx +4 -5
- package/src/components/ui/CommandPalette.tsx +58 -36
- package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
- package/src/components/ui/Dropdown.tsx +18 -16
- package/src/components/ui/EmptyState.tsx +25 -0
- package/src/components/ui/GlobalModal.tsx +49 -0
- package/src/components/ui/IconButton.tsx +44 -0
- package/src/components/ui/Modal.tsx +19 -20
- package/src/components/ui/PageHeader.tsx +158 -0
- package/src/components/ui/Pagination.tsx +61 -0
- package/src/components/ui/PromptModal.tsx +1 -1
- package/src/components/ui/SearchInput.tsx +57 -0
- package/src/components/ui/SeoPreview.tsx +31 -0
- package/src/components/ui/SessionModal.tsx +0 -0
- package/src/components/ui/SlidePanel.tsx +2 -0
- package/src/components/ui/Toast.tsx +65 -122
- package/src/components/ui/Toaster.tsx +18 -0
- package/src/components/ui/icons.tsx +112 -0
- package/src/components/users/UserDetail.tsx +290 -0
- package/src/components/users/UserForm.tsx +242 -0
- package/src/components/users/UsersList.tsx +338 -0
- package/src/env.d.ts +13 -13
- package/src/fields/index.ts +2 -1
- package/src/global.d.ts +7 -0
- package/src/hooks/data.ts +2 -9
- package/src/hooks/useAsyncData.ts +36 -0
- package/src/hooks/useAutoFormState.ts +527 -0
- package/src/hooks/useSelection.ts +49 -0
- package/src/hooks/useSession.ts +0 -0
- package/src/index.ts +11 -1
- package/src/integration.ts +86 -11
- package/src/kyro-cms.d.ts +209 -0
- package/src/layouts/AdminLayout.astro +128 -11
- package/src/layouts/AuthLayout.astro +21 -5
- package/src/lib/api.ts +175 -55
- package/src/lib/autoform-store.ts +435 -0
- package/src/lib/config.ts +82 -34
- package/src/lib/createRegistry.ts +29 -0
- package/src/lib/default-kyro-config.ts +4 -0
- package/src/lib/globals.ts +50 -0
- package/src/lib/media-utils.ts +18 -0
- package/src/lib/object-utils.ts +77 -0
- package/src/lib/paths.ts +61 -0
- package/src/lib/stores/index.ts +370 -0
- package/src/lib/types.ts +43 -0
- package/src/lib/useResourceManager.ts +105 -0
- package/src/pages/403.astro +67 -0
- package/src/pages/[collection]/[id].astro +14 -180
- package/src/pages/[collection]/index.astro +11 -6
- package/src/pages/api-explorer.astro +173 -0
- package/src/pages/audit/index.astro +2 -0
- package/src/pages/auth/login.astro +122 -0
- package/src/pages/auth/register.astro +167 -0
- package/src/pages/graphql-explorer.astro +59 -0
- package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
- package/src/pages/index.astro +577 -0
- package/src/pages/index_ALT.astro +3 -0
- package/src/pages/keys.astro +11 -0
- package/src/pages/marketplace.astro +11 -0
- package/src/pages/media.astro +3 -0
- package/src/pages/plugins.astro +8 -0
- package/src/pages/preview/[collection]/[id].astro +188 -123
- package/src/pages/rest-playground.astro +62 -0
- package/src/pages/roles/index.astro +183 -76
- package/src/pages/sessions.astro +8 -0
- package/src/pages/settings/[slug].astro +92 -114
- package/src/pages/settings/index.astro +5 -3
- package/src/pages/users/[id].astro +25 -154
- package/src/pages/users/index.astro +19 -130
- package/src/pages/users/new.astro +9 -86
- package/src/pages/webhooks.astro +11 -0
- package/src/routes.ts +80 -0
- package/src/styles/main.css +119 -79
- package/src/theme/tokens.ts +1 -0
- package/src/vite-env.d.ts +14 -0
- package/src/collections/auth/index.ts +0 -155
- package/src/collections/portfolio/index.ts +0 -343
- package/src/components/ApiExplorer.tsx +0 -325
- package/src/components/EnhancedListView.tsx +0 -889
- package/src/components/GraphQLExplorer.tsx +0 -675
- package/src/components/Icons.tsx +0 -23
- package/src/components/StatusBadge.tsx +0 -76
- package/src/lib/MediaService.ts +0 -541
- package/src/lib/auth/sqlite-adapter.ts +0 -319
- package/src/lib/dataStore.ts +0 -226
- package/src/lib/db/adapter.ts +0 -54
- package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
- package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
- package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
- package/src/lib/db/index.ts +0 -449
- package/src/lib/db/mongodb-adapter.ts +0 -207
- package/src/lib/db/mongodb-auth-adapter.ts +0 -305
- package/src/lib/db/schema/mysql-auth.ts +0 -113
- package/src/lib/db/schema/mysql-content.ts +0 -20
- package/src/lib/db/schema/postgres-auth.ts +0 -116
- package/src/lib/db/schema/postgres-content.ts +0 -35
- package/src/lib/db/schema/postgres-media.ts +0 -52
- package/src/lib/db/schema/postgres-settings.ts +0 -11
- package/src/lib/db/schema/sqlite-auth.ts +0 -112
- package/src/lib/db/schema/sqlite-content.ts +0 -20
- package/src/lib/db/version-adapter.ts +0 -248
- package/src/lib/graphql/index.ts +0 -1
- package/src/lib/graphql/schema.ts +0 -443
- package/src/lib/rate-limit.ts +0 -267
- package/src/lib/storage.ts +0 -374
- package/src/lib/store.ts +0 -85
- package/src/middleware.ts +0 -177
- package/src/pages/admin/api-explorer.astro +0 -98
- package/src/pages/admin/graphql-explorer.astro +0 -40
- package/src/pages/admin/index.astro +0 -286
- package/src/pages/admin/keys.astro +0 -8
- package/src/pages/admin/rest-playground.astro +0 -44
- package/src/pages/admin/webhooks.astro +0 -8
- package/src/pages/api/[collection]/[id]/publish.ts +0 -52
- package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
- package/src/pages/api/[collection]/[id]/versions.ts +0 -66
- package/src/pages/api/[collection]/[id].ts +0 -213
- package/src/pages/api/[collection]/index.ts +0 -209
- package/src/pages/api/auth/[id].ts +0 -121
- package/src/pages/api/auth/audit-logs.ts +0 -57
- package/src/pages/api/auth/login.ts +0 -211
- package/src/pages/api/auth/logout.ts +0 -66
- package/src/pages/api/auth/me.ts +0 -36
- package/src/pages/api/auth/refresh.ts +0 -119
- package/src/pages/api/auth/register.ts +0 -188
- package/src/pages/api/auth/users.ts +0 -97
- package/src/pages/api/collections.ts +0 -59
- package/src/pages/api/globals/[slug].ts +0 -42
- package/src/pages/api/graphql.ts +0 -90
- package/src/pages/api/health.ts +0 -426
- package/src/pages/api/keys/[id].ts +0 -26
- package/src/pages/api/keys/index.ts +0 -75
- package/src/pages/api/media/[id].ts +0 -309
- package/src/pages/api/media/folders.ts +0 -609
- package/src/pages/api/media/index.ts +0 -146
- package/src/pages/api/media/resize.ts +0 -267
- package/src/pages/api/search.ts +0 -82
- package/src/pages/api/slug-availability.ts +0 -70
- package/src/pages/api/storage-config.ts +0 -20
- package/src/pages/api/storage-status.ts +0 -206
- package/src/pages/api/upload.ts +0 -334
- package/src/pages/api/webhooks/index.ts +0 -71
- package/src/pages/login.astro +0 -82
- package/src/pages/register.astro +0 -102
|
@@ -1,161 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "
|
|
8
|
-
import {
|
|
9
|
-
defaultLightTheme,
|
|
10
|
-
defaultDarkTheme,
|
|
11
|
-
type ThemeConfig,
|
|
12
|
-
} from "@kyro-cms/core/client";
|
|
13
|
-
|
|
14
|
-
export type ThemeMode = "light" | "dark" | "system";
|
|
15
|
-
|
|
16
|
-
interface ThemeContextValue {
|
|
17
|
-
mode: ThemeMode;
|
|
18
|
-
theme: ThemeConfig;
|
|
19
|
-
setMode: (mode: ThemeMode) => void;
|
|
20
|
-
setCustomTheme: (theme: ThemeConfig) => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
24
|
-
|
|
25
|
-
export function useTheme() {
|
|
26
|
-
const context = useContext(ThemeContext);
|
|
27
|
-
if (!context) {
|
|
28
|
-
// Return default light theme if used outside of a provider to prevent crashes
|
|
29
|
-
return {
|
|
30
|
-
mode: "light" as ThemeMode,
|
|
31
|
-
theme: defaultLightTheme,
|
|
32
|
-
setMode: () => {},
|
|
33
|
-
setCustomTheme: () => {},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
return context;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface ThemeProviderProps {
|
|
40
|
-
children: ReactNode;
|
|
41
|
-
defaultMode?: ThemeMode;
|
|
42
|
-
themes?: {
|
|
43
|
-
light?: ThemeConfig;
|
|
44
|
-
dark?: ThemeConfig;
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function ThemeProvider({
|
|
49
|
-
children,
|
|
50
|
-
defaultMode = "light",
|
|
51
|
-
themes = {},
|
|
52
|
-
}: ThemeProviderProps) {
|
|
53
|
-
const [mode, setMode] = useState<ThemeMode>(defaultMode);
|
|
54
|
-
const [customTheme, setCustomTheme] = useState<ThemeConfig | null>(null);
|
|
55
|
-
|
|
56
|
-
const lightTheme = themes.light || defaultLightTheme;
|
|
57
|
-
const darkTheme = themes.dark || defaultDarkTheme;
|
|
58
|
-
|
|
59
|
-
const getResolvedTheme = (): ThemeConfig => {
|
|
60
|
-
if (customTheme) return customTheme;
|
|
61
|
-
|
|
62
|
-
if (mode === "system") {
|
|
63
|
-
if (typeof window !== "undefined") {
|
|
64
|
-
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
65
|
-
? darkTheme
|
|
66
|
-
: lightTheme;
|
|
67
|
-
}
|
|
68
|
-
return lightTheme;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return mode === "dark" ? darkTheme : lightTheme;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const [theme, setTheme] = useState<ThemeConfig>(lightTheme);
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
const resolved = getResolvedTheme();
|
|
78
|
-
setTheme(resolved);
|
|
79
|
-
applyThemeVariables(resolved);
|
|
80
|
-
}, [mode, customTheme]);
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (mode !== "system") return;
|
|
84
|
-
|
|
85
|
-
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
86
|
-
const handler = () => {
|
|
87
|
-
const resolved = getResolvedTheme();
|
|
88
|
-
setTheme(resolved);
|
|
89
|
-
applyThemeVariables(resolved);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
mediaQuery.addEventListener("change", handler);
|
|
93
|
-
return () => mediaQuery.removeEventListener("change", handler);
|
|
94
|
-
}, [mode, customTheme]);
|
|
95
|
-
|
|
96
|
-
const applyThemeVariables = (config: ThemeConfig) => {
|
|
97
|
-
const root = document.documentElement;
|
|
98
|
-
|
|
99
|
-
if (config.colors) {
|
|
100
|
-
Object.entries(config.colors).forEach(([key, value]) => {
|
|
101
|
-
root.style.setProperty(`--kyro-${key}`, value);
|
|
102
|
-
root.style.setProperty(
|
|
103
|
-
`--kyro-${key}-light`,
|
|
104
|
-
adjustBrightness(value, 0.9),
|
|
105
|
-
);
|
|
106
|
-
root.style.setProperty(
|
|
107
|
-
`--kyro-${key}-dark`,
|
|
108
|
-
adjustBrightness(value, 0.8),
|
|
109
|
-
);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (config.borderRadius) {
|
|
114
|
-
Object.entries(config.borderRadius).forEach(([key, value]) => {
|
|
115
|
-
root.style.setProperty(`--kyro-radius-${key}`, value);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (config.fonts) {
|
|
120
|
-
Object.entries(config.fonts).forEach(([key, value]) => {
|
|
121
|
-
root.style.setProperty(`--kyro-font-${key}`, value);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const adjustBrightness = (hex: string, factor: number): string => {
|
|
127
|
-
if (!hex.startsWith("#")) return hex;
|
|
128
|
-
|
|
129
|
-
const r = parseInt(hex.slice(1, 3), 16);
|
|
130
|
-
const g = parseInt(hex.slice(3, 5), 16);
|
|
131
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
132
|
-
|
|
133
|
-
const adjust = (c: number) =>
|
|
134
|
-
Math.round(c * factor)
|
|
135
|
-
.toString(16)
|
|
136
|
-
.padStart(2, "0");
|
|
137
|
-
|
|
138
|
-
return `#${adjust(r)}${adjust(g)}${adjust(b)}`;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<ThemeContext.Provider
|
|
143
|
-
value={{
|
|
144
|
-
mode,
|
|
145
|
-
theme,
|
|
146
|
-
setMode,
|
|
147
|
-
setCustomTheme,
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
150
|
-
{children}
|
|
151
|
-
</ThemeContext.Provider>
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export const LightThemeProvider = (
|
|
156
|
-
props: Omit<ThemeProviderProps, "defaultMode">,
|
|
157
|
-
) => <ThemeProvider defaultMode="light" {...props} />;
|
|
158
|
-
|
|
159
|
-
export const DarkThemeProvider = (
|
|
160
|
-
props: Omit<ThemeProviderProps, "defaultMode">,
|
|
161
|
-
) => <ThemeProvider defaultMode="dark" {...props} />;
|
|
1
|
+
export {
|
|
2
|
+
ThemeProvider,
|
|
3
|
+
LightThemeProvider,
|
|
4
|
+
DarkThemeProvider,
|
|
5
|
+
useTheme,
|
|
6
|
+
} from "../theme/ThemeProvider";
|
|
7
|
+
export type { ThemeMode } from "../theme/ThemeProvider";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { apiGet, apiPatch } from "../lib/api";
|
|
2
|
+
import { apiGet, apiPatch, apiDelete } from "../lib/api";
|
|
3
3
|
import {
|
|
4
4
|
Users,
|
|
5
5
|
UserPlus,
|
|
@@ -11,7 +11,15 @@ import {
|
|
|
11
11
|
Clock,
|
|
12
12
|
Search,
|
|
13
13
|
Filter,
|
|
14
|
-
|
|
14
|
+
Trash2,
|
|
15
|
+
Edit2,
|
|
16
|
+
ChevronRight,
|
|
17
|
+
ShieldCheck,
|
|
18
|
+
ShieldAlert
|
|
19
|
+
} from "./ui/icons";
|
|
20
|
+
import { useUIStore, toast } from "../lib/stores";
|
|
21
|
+
import { Badge } from "./ui/Badge";
|
|
22
|
+
import { PageHeader } from "./ui/PageHeader";
|
|
15
23
|
|
|
16
24
|
interface User {
|
|
17
25
|
id: string;
|
|
@@ -20,6 +28,7 @@ interface User {
|
|
|
20
28
|
role: string;
|
|
21
29
|
locked?: boolean;
|
|
22
30
|
lastLogin?: string;
|
|
31
|
+
tenantId?: string;
|
|
23
32
|
createdAt: string;
|
|
24
33
|
}
|
|
25
34
|
|
|
@@ -27,6 +36,7 @@ export function UserManagement() {
|
|
|
27
36
|
const [users, setUsers] = useState<User[]>([]);
|
|
28
37
|
const [loading, setLoading] = useState(true);
|
|
29
38
|
const [searchQuery, setSearchQuery] = useState("");
|
|
39
|
+
const { confirm, alert } = useUIStore();
|
|
30
40
|
|
|
31
41
|
useEffect(() => {
|
|
32
42
|
loadUsers();
|
|
@@ -35,7 +45,7 @@ export function UserManagement() {
|
|
|
35
45
|
const loadUsers = async () => {
|
|
36
46
|
try {
|
|
37
47
|
setLoading(true);
|
|
38
|
-
const result = await apiGet("/api/
|
|
48
|
+
const result = await apiGet<any>("/api/users");
|
|
39
49
|
setUsers(result.docs || []);
|
|
40
50
|
} catch (error) {
|
|
41
51
|
console.error("Failed to load users:", error);
|
|
@@ -44,15 +54,44 @@ export function UserManagement() {
|
|
|
44
54
|
}
|
|
45
55
|
};
|
|
46
56
|
|
|
47
|
-
const handleToggleLock =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
const handleToggleLock = (user: User) => {
|
|
58
|
+
const isLocking = !user.locked;
|
|
59
|
+
confirm({
|
|
60
|
+
title: isLocking ? "Lock User Account?" : "Unlock User Account?",
|
|
61
|
+
message: isLocking
|
|
62
|
+
? `Are you sure you want to lock ${user.email}? They will be immediately logged out and unable to return.`
|
|
63
|
+
: `Restore system access for ${user.email}?`,
|
|
64
|
+
variant: isLocking ? "danger" : "success",
|
|
65
|
+
onConfirm: async () => {
|
|
66
|
+
try {
|
|
67
|
+
await apiPatch(`/api/users/${user.id}`, { locked: isLocking });
|
|
68
|
+
setUsers((prev) =>
|
|
69
|
+
prev.map((u) => (u.id === user.id ? { ...u, locked: isLocking } : u)),
|
|
70
|
+
);
|
|
71
|
+
toast.success(isLocking ? `Account locked: ${user.email}` : `Account restored: ${user.email}`);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error("Failed to toggle user lock:", error);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleDelete = (user: User) => {
|
|
80
|
+
confirm({
|
|
81
|
+
title: "Destroy User Account",
|
|
82
|
+
message: `You are about to permanently delete ${user.email}. This will remove all their data and cannot be undone.`,
|
|
83
|
+
variant: "danger",
|
|
84
|
+
confirmLabel: "Destroy Account",
|
|
85
|
+
onConfirm: async () => {
|
|
86
|
+
try {
|
|
87
|
+
await apiDelete(`/api/users/${user.id}`);
|
|
88
|
+
setUsers((prev) => prev.filter((u) => u.id !== user.id));
|
|
89
|
+
toast.success(`Identity purged: ${user.email}`);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error("Failed to delete user:", error);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
56
95
|
};
|
|
57
96
|
|
|
58
97
|
const filteredUsers = users.filter(
|
|
@@ -62,152 +101,129 @@ export function UserManagement() {
|
|
|
62
101
|
);
|
|
63
102
|
|
|
64
103
|
return (
|
|
65
|
-
<div className="w-full space-y-
|
|
104
|
+
<div className="w-full space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-12">
|
|
66
105
|
{/* Header */}
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-2xl font-black text-sm shadow-xl active:scale-95 transition-all"
|
|
80
|
-
>
|
|
81
|
-
<UserPlus className="w-4 h-4" />
|
|
82
|
-
Invite Member
|
|
83
|
-
</button>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
106
|
+
<PageHeader
|
|
107
|
+
title="Identity & Access"
|
|
108
|
+
description="Manage the core administrative team and security permissions."
|
|
109
|
+
icon={Users}
|
|
110
|
+
action={{
|
|
111
|
+
label: "New User",
|
|
112
|
+
onClick: () => {
|
|
113
|
+
// New user logic
|
|
114
|
+
},
|
|
115
|
+
icon: UserPlus,
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
86
118
|
|
|
87
|
-
{/*
|
|
88
|
-
<div className="flex flex-col md:flex-row gap-
|
|
119
|
+
{/* Control Bar */}
|
|
120
|
+
<div className="flex flex-col md:flex-row gap-3">
|
|
89
121
|
<div className="relative flex-1 group">
|
|
90
|
-
<Search className="absolute left-
|
|
122
|
+
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity" />
|
|
91
123
|
<input
|
|
92
124
|
type="text"
|
|
93
|
-
placeholder="Search by
|
|
125
|
+
placeholder="Search by identity or email..."
|
|
94
126
|
value={searchQuery}
|
|
95
127
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
96
|
-
className="w-full pl-
|
|
128
|
+
className="w-full pl-10 pr-4 py-2.5 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] transition-all text-xs font-bold"
|
|
97
129
|
/>
|
|
98
130
|
</div>
|
|
99
|
-
<div className="flex items-center gap-
|
|
100
|
-
<button
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
>
|
|
104
|
-
All Users
|
|
105
|
-
</button>
|
|
106
|
-
<button
|
|
107
|
-
type="button"
|
|
108
|
-
className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all"
|
|
109
|
-
>
|
|
110
|
-
Admins
|
|
111
|
-
</button>
|
|
112
|
-
<button
|
|
113
|
-
type="button"
|
|
114
|
-
className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all"
|
|
115
|
-
>
|
|
116
|
-
Restricted
|
|
117
|
-
</button>
|
|
131
|
+
<div className="flex items-center gap-1 bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
|
|
132
|
+
<button className="px-4 py-1.5 text-[10px] font-bold tracking-widest bg-[var(--kyro-surface)] shadow-sm rounded-lg border border-[var(--kyro-border)]">ALL</button>
|
|
133
|
+
<button className="px-4 py-1.5 text-[10px] font-bold tracking-widest opacity-40 hover:opacity-100 transition-all">ADMINS</button>
|
|
134
|
+
<button className="px-4 py-1.5 text-[10px] font-bold tracking-widest opacity-40 hover:opacity-100 transition-all">LOCKED</button>
|
|
118
135
|
</div>
|
|
119
136
|
</div>
|
|
120
137
|
|
|
121
|
-
{/* User
|
|
122
|
-
<div className="
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
.
|
|
126
|
-
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)}
|
|
138
|
+
{/* User Table */}
|
|
139
|
+
<div className="surface-tile overflow-hidden">
|
|
140
|
+
<table className="w-full text-left table-fixed">
|
|
141
|
+
<thead>
|
|
142
|
+
<tr className="text-[var(--kyro-text-secondary)] font-bold text-[9px] tracking-[0.2em] uppercase border-b border-[var(--kyro-border)]">
|
|
143
|
+
<th className="px-6 py-4 w-64">Member Identity</th>
|
|
144
|
+
<th className="px-6 py-4">Administrative Role</th>
|
|
145
|
+
<th className="px-6 py-4">Security Status</th>
|
|
146
|
+
<th className="px-6 py-4">Last Activity</th>
|
|
147
|
+
<th className="px-6 py-4 w-32 text-right">Actions</th>
|
|
148
|
+
</tr>
|
|
149
|
+
</thead>
|
|
150
|
+
<tbody className="divide-y divide-[var(--kyro-border)]">
|
|
151
|
+
{loading ? (
|
|
152
|
+
Array.from({ length: 6 }).map((_, i) => (
|
|
153
|
+
<tr key={i} className="animate-pulse">
|
|
154
|
+
<td colSpan={5} className="px-6 py-5 bg-[var(--kyro-surface-accent)]/30" />
|
|
155
|
+
</tr>
|
|
156
|
+
))
|
|
157
|
+
) : filteredUsers.length === 0 ? (
|
|
158
|
+
<tr>
|
|
159
|
+
<td colSpan={5} className="px-6 py-20 text-center">
|
|
160
|
+
<p className="text-xs font-bold opacity-30 tracking-widest uppercase italic">No identity matches found</p>
|
|
161
|
+
</td>
|
|
162
|
+
</tr>
|
|
163
|
+
) : (
|
|
164
|
+
filteredUsers.map((user) => (
|
|
165
|
+
<tr key={user.id} className={`hover:bg-[var(--kyro-surface-accent)]/50 transition-colors group ${user.locked ? "opacity-50 grayscale" : ""}`}>
|
|
166
|
+
<td className="px-6 py-3.5">
|
|
167
|
+
<div className="flex items-center gap-3">
|
|
168
|
+
<div className="w-8 h-8 rounded-lg bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] flex items-center justify-center text-xs font-bold text-[var(--kyro-primary)] group-hover:scale-105 transition-transform">
|
|
169
|
+
{user.name ? user.name[0] : user.email[0].toUpperCase()}
|
|
170
|
+
</div>
|
|
171
|
+
<div className="min-w-0">
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<div className="text-xs font-bold text-[var(--kyro-text-primary)] truncate">{user.name || user.email.split("@")[0]}</div>
|
|
174
|
+
{user.tenantId && (
|
|
175
|
+
<Badge variant="outline" className="text-[7px] px-1 py-0 border-none bg-[var(--kyro-surface-accent)] opacity-50">
|
|
176
|
+
{user.tenantId}
|
|
177
|
+
</Badge>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
<div className="text-[10px] text-[var(--kyro-text-secondary)] opacity-50 truncate">{user.email}</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</td>
|
|
184
|
+
<td className="px-6 py-3.5">
|
|
185
|
+
<div className="flex items-center gap-2">
|
|
186
|
+
<Shield className="w-3.5 h-3.5 opacity-30" />
|
|
187
|
+
<span className="text-[10px] font-bold tracking-widest uppercase opacity-70">{user.role}</span>
|
|
188
|
+
</div>
|
|
189
|
+
</td>
|
|
190
|
+
<td className="px-6 py-3.5">
|
|
191
|
+
<Badge variant={user.locked ? "danger" : "success"} dot className="text-[8px] font-bold uppercase tracking-widest">
|
|
192
|
+
{user.locked ? "Restricted" : "Authorized"}
|
|
193
|
+
</Badge>
|
|
194
|
+
</td>
|
|
195
|
+
<td className="px-6 py-3.5">
|
|
196
|
+
<div className="flex items-center gap-2 text-[10px] font-bold text-[var(--kyro-text-secondary)] opacity-50 uppercase tabular-nums">
|
|
197
|
+
<Clock className="w-3 h-3" />
|
|
198
|
+
{user.lastLogin ? new Date(user.lastLogin).toLocaleDateString() : "Never"}
|
|
199
|
+
</div>
|
|
200
|
+
</td>
|
|
201
|
+
<td className="px-6 py-3.5 text-right">
|
|
202
|
+
<div className="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-all">
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => handleToggleLock(user)}
|
|
205
|
+
className={`p-1.5 rounded-lg border transition-all ${user.locked ? "bg-green-500/10 text-green-500 border-green-500/20 hover:bg-green-500/20" : "bg-amber-500/10 text-amber-500 border-amber-500/20 hover:bg-amber-500/20"}`}
|
|
206
|
+
title={user.locked ? "Restore Access" : "Restrict Access"}
|
|
207
|
+
>
|
|
208
|
+
{user.locked ? <Unlock className="w-3.5 h-3.5" /> : <Lock className="w-3.5 h-3.5" />}
|
|
209
|
+
</button>
|
|
210
|
+
<button
|
|
211
|
+
onClick={() => handleDelete(user)}
|
|
212
|
+
className="p-1.5 rounded-lg border border-red-500/20 bg-red-500/10 text-red-500 hover:bg-red-500/20 transition-all"
|
|
213
|
+
title="Delete User"
|
|
214
|
+
>
|
|
215
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
216
|
+
</button>
|
|
217
|
+
<button className="p-1.5 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface)] transition-all">
|
|
218
|
+
<MoreVertical className="w-3.5 h-3.5" />
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
</td>
|
|
222
|
+
</tr>
|
|
223
|
+
))
|
|
224
|
+
)}
|
|
225
|
+
</tbody>
|
|
226
|
+
</table>
|
|
211
227
|
</div>
|
|
212
228
|
</div>
|
|
213
229
|
);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Dropdown, DropdownItem, DropdownSeparator } from "./ui/Dropdown";
|
|
3
|
+
import { User, Shield, Key, Webhook, Clock, FileText, ExternalLink, HelpCircle, LogOut } from "./ui/icons";
|
|
4
|
+
|
|
5
|
+
interface UserMenuProps {
|
|
6
|
+
adminPath: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function UserMenu({ adminPath }: UserMenuProps) {
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Dropdown
|
|
13
|
+
align="right"
|
|
14
|
+
trigger={
|
|
15
|
+
<div
|
|
16
|
+
className="flex justify-center p-2.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] rounded-xl transition-all shadow-sm active:scale-95"
|
|
17
|
+
title="Account"
|
|
18
|
+
>
|
|
19
|
+
<User className="w-4 h-4" strokeWidth={2.5} />
|
|
20
|
+
</div>
|
|
21
|
+
}
|
|
22
|
+
>
|
|
23
|
+
<div className="px-4 py-2 mb-1">
|
|
24
|
+
<p className="text-[10px] font-medium tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
|
|
25
|
+
Account
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<DropdownItem
|
|
30
|
+
icon={<User className="w-4 h-4" />}
|
|
31
|
+
onClick={() => window.location.href = `${adminPath}/settings/account`}
|
|
32
|
+
>
|
|
33
|
+
Profile Settings
|
|
34
|
+
</DropdownItem>
|
|
35
|
+
|
|
36
|
+
<DropdownItem
|
|
37
|
+
icon={<Shield className="w-4 h-4" />}
|
|
38
|
+
onClick={() => window.location.href = `${adminPath}/roles`}
|
|
39
|
+
>
|
|
40
|
+
Permissions
|
|
41
|
+
</DropdownItem>
|
|
42
|
+
|
|
43
|
+
<DropdownSeparator />
|
|
44
|
+
|
|
45
|
+
<div className="px-4 py-2 mb-1">
|
|
46
|
+
<p className="text-[10px] font-medium tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
|
|
47
|
+
Developer
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<DropdownItem
|
|
52
|
+
icon={<Key className="w-4 h-4" />}
|
|
53
|
+
onClick={() => (window.location.href = `${adminPath}/keys`)}
|
|
54
|
+
>
|
|
55
|
+
API Keys
|
|
56
|
+
</DropdownItem>
|
|
57
|
+
|
|
58
|
+
<DropdownItem
|
|
59
|
+
icon={<Webhook className="w-4 h-4" />}
|
|
60
|
+
onClick={() => (window.location.href = `${adminPath}/webhooks`)}
|
|
61
|
+
>
|
|
62
|
+
Web Hooks
|
|
63
|
+
</DropdownItem>
|
|
64
|
+
<DropdownItem
|
|
65
|
+
icon={<Clock className="w-4 h-4" />}
|
|
66
|
+
onClick={() => (window.location.href = `${adminPath}/sessions`)}
|
|
67
|
+
>
|
|
68
|
+
Sessions
|
|
69
|
+
</DropdownItem>
|
|
70
|
+
<DropdownItem
|
|
71
|
+
icon={<FileText className="w-4 h-4" />}
|
|
72
|
+
onClick={() => (window.location.href = `${adminPath}/audit`)}
|
|
73
|
+
>
|
|
74
|
+
Audit Logs
|
|
75
|
+
</DropdownItem>
|
|
76
|
+
|
|
77
|
+
<DropdownSeparator />
|
|
78
|
+
|
|
79
|
+
<div className="px-4 py-2 mb-1">
|
|
80
|
+
<p className="text-[10px] font-medium tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
|
|
81
|
+
Resources
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<DropdownItem
|
|
86
|
+
icon={<ExternalLink className="w-4 h-4" />}
|
|
87
|
+
onClick={() => window.open("https://docs.kyro.dev", "_blank")}
|
|
88
|
+
>
|
|
89
|
+
Documentation
|
|
90
|
+
</DropdownItem>
|
|
91
|
+
|
|
92
|
+
<DropdownItem
|
|
93
|
+
icon={<HelpCircle className="w-4 h-4" />}
|
|
94
|
+
onClick={() => window.open("https://github.com/danielDozie/kyro-cms/issues", "_blank")}
|
|
95
|
+
>
|
|
96
|
+
Get Support
|
|
97
|
+
</DropdownItem>
|
|
98
|
+
|
|
99
|
+
<DropdownSeparator />
|
|
100
|
+
|
|
101
|
+
<DropdownItem
|
|
102
|
+
icon={<LogOut className="w-4 h-4" />}
|
|
103
|
+
danger
|
|
104
|
+
onClick={() => document.getElementById("logout-btn")?.click()}
|
|
105
|
+
>
|
|
106
|
+
Sign Out
|
|
107
|
+
</DropdownItem>
|
|
108
|
+
</Dropdown>
|
|
109
|
+
);
|
|
110
|
+
}
|