mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
feat: initial ui redesign from demo
This commit is contained in:
@@ -0,0 +1,498 @@
|
||||
import React from "react";
|
||||
import { useSidebar } from "@/components/sidebar.tsx";
|
||||
import { Separator } from "@/components/separator.tsx";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/tabs.tsx";
|
||||
import { Shield, Users, Database, Clock, Key } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
import {
|
||||
getAdminOIDCConfig,
|
||||
getRegistrationAllowed,
|
||||
getPasswordLoginAllowed,
|
||||
getPasswordResetAllowed,
|
||||
getUserList,
|
||||
getUserInfo,
|
||||
isElectron,
|
||||
getSessions,
|
||||
unlinkOIDCFromPasswordAccount,
|
||||
} from "@/main-axios.ts";
|
||||
import { SimpleLoader } from "@/lib/SimpleLoader.tsx";
|
||||
import { RolesTab } from "@/admin/tabs/RolesTab.tsx";
|
||||
import { GeneralSettingsTab } from "@/admin/tabs/GeneralSettingsTab.tsx";
|
||||
import { OIDCSettingsTab } from "@/admin/tabs/OIDCSettingsTab.tsx";
|
||||
import { UserManagementTab } from "@/admin/tabs/UserManagementTab.tsx";
|
||||
import { SessionManagementTab } from "@/admin/tabs/SessionManagementTab.tsx";
|
||||
import { DatabaseSecurityTab } from "@/admin/tabs/DatabaseSecurityTab.tsx";
|
||||
import { ApiKeysTab } from "@/admin/tabs/ApiKeysTab.tsx";
|
||||
import { CreateUserDialog } from "./dialogs/CreateUserDialog.tsx";
|
||||
import { UserEditDialog } from "./dialogs/UserEditDialog.tsx";
|
||||
import { LinkAccountDialog } from "./dialogs/LinkAccountDialog.tsx";
|
||||
|
||||
interface AdminSettingsProps {
|
||||
isTopbarOpen?: boolean;
|
||||
rightSidebarOpen?: boolean;
|
||||
rightSidebarWidth?: number;
|
||||
}
|
||||
|
||||
export function AdminSettings({
|
||||
isTopbarOpen = true,
|
||||
rightSidebarOpen = false,
|
||||
rightSidebarWidth = 400,
|
||||
}: AdminSettingsProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { confirmWithToast } = useConfirmation();
|
||||
const { state: sidebarState } = useSidebar();
|
||||
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [allowRegistration, setAllowRegistration] = React.useState(true);
|
||||
const [allowPasswordLogin, setAllowPasswordLogin] = React.useState(true);
|
||||
const [allowPasswordReset, setAllowPasswordReset] = React.useState(true);
|
||||
|
||||
const [oidcConfig, setOidcConfig] = React.useState({
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
issuer_url: "",
|
||||
authorization_url: "",
|
||||
token_url: "",
|
||||
identifier_path: "sub",
|
||||
name_path: "name",
|
||||
scopes: "openid email profile",
|
||||
userinfo_url: "",
|
||||
allowed_users: "",
|
||||
});
|
||||
|
||||
const [users, setUsers] = React.useState<
|
||||
Array<{
|
||||
id: string;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
isOidc: boolean;
|
||||
passwordHash?: string;
|
||||
}>
|
||||
>([]);
|
||||
const [usersLoading, setUsersLoading] = React.useState(false);
|
||||
|
||||
const [createUserDialogOpen, setCreateUserDialogOpen] = React.useState(false);
|
||||
const [userEditDialogOpen, setUserEditDialogOpen] = React.useState(false);
|
||||
const [selectedUserForEdit, setSelectedUserForEdit] = React.useState<{
|
||||
id: string;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
isOidc: boolean;
|
||||
passwordHash?: string;
|
||||
} | null>(null);
|
||||
|
||||
const [currentUser, setCurrentUser] = React.useState<{
|
||||
id: string;
|
||||
username: string;
|
||||
is_admin: boolean;
|
||||
is_oidc: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const [sessions, setSessions] = React.useState<
|
||||
Array<{
|
||||
id: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
deviceType: string;
|
||||
deviceInfo: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
lastActiveAt: string;
|
||||
isRevoked?: boolean;
|
||||
isCurrentSession?: boolean;
|
||||
}>
|
||||
>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = React.useState(false);
|
||||
|
||||
const [linkAccountAlertOpen, setLinkAccountAlertOpen] = React.useState(false);
|
||||
const [linkOidcUser, setLinkOidcUser] = React.useState<{
|
||||
id: string;
|
||||
username: string;
|
||||
} | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Promise.allSettled([
|
||||
getAdminOIDCConfig()
|
||||
.then((res) => {
|
||||
if (res) setOidcConfig(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err.message?.includes("No server configured")) {
|
||||
toast.error(t("admin.failedToFetchOidcConfig"));
|
||||
}
|
||||
}),
|
||||
getUserInfo()
|
||||
.then((info) => {
|
||||
if (info) {
|
||||
setCurrentUser({
|
||||
id: info.userId,
|
||||
username: info.username,
|
||||
is_admin: info.is_admin,
|
||||
is_oidc: info.is_oidc,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err?.message?.includes("No server configured")) {
|
||||
console.warn("Failed to fetch current user info", err);
|
||||
}
|
||||
}),
|
||||
getSessions()
|
||||
.then((data) => setSessions(data.sessions || []))
|
||||
.catch((err) => {
|
||||
if (!err?.message?.includes("No server configured")) {
|
||||
toast.error(t("admin.failedToFetchSessions"));
|
||||
}
|
||||
}),
|
||||
]).finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getRegistrationAllowed()
|
||||
.then((res) => {
|
||||
if (typeof res?.allowed === "boolean") {
|
||||
setAllowRegistration(res.allowed);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err.message?.includes("No server configured")) {
|
||||
toast.error(t("admin.failedToFetchRegistrationStatus"));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getPasswordLoginAllowed()
|
||||
.then((res) => {
|
||||
if (typeof res?.allowed === "boolean") {
|
||||
setAllowPasswordLogin(res.allowed);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code !== "NO_SERVER_CONFIGURED") {
|
||||
toast.error(t("admin.failedToFetchPasswordLoginStatus"));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getPasswordResetAllowed()
|
||||
.then((res) => {
|
||||
if (typeof res === "boolean") {
|
||||
setAllowPasswordReset(res);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code !== "NO_SERVER_CONFIGURED") {
|
||||
console.warn("Failed to fetch password reset status", err);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setUsersLoading(true);
|
||||
try {
|
||||
const response = await getUserList();
|
||||
setUsers(response.users);
|
||||
} catch (err) {
|
||||
if (!err.message?.includes("No server configured")) {
|
||||
toast.error(t("admin.failedToFetchUsers"));
|
||||
}
|
||||
} finally {
|
||||
setUsersLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditUser = (user: (typeof users)[0]) => {
|
||||
setSelectedUserForEdit(user);
|
||||
setUserEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateUserSuccess = () => {
|
||||
fetchUsers();
|
||||
setCreateUserDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleEditUserSuccess = () => {
|
||||
fetchUsers();
|
||||
setUserEditDialogOpen(false);
|
||||
setSelectedUserForEdit(null);
|
||||
};
|
||||
|
||||
const fetchSessions = async () => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSessionsLoading(true);
|
||||
try {
|
||||
const data = await getSessions();
|
||||
setSessions(data.sessions || []);
|
||||
} catch (err) {
|
||||
if (!err?.message?.includes("No server configured")) {
|
||||
toast.error(t("admin.failedToFetchSessions"));
|
||||
}
|
||||
} finally {
|
||||
setSessionsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinkOIDCUser = (user: { id: string; username: string }) => {
|
||||
setLinkOidcUser(user);
|
||||
setLinkAccountAlertOpen(true);
|
||||
};
|
||||
|
||||
const handleLinkSuccess = () => {
|
||||
fetchUsers();
|
||||
fetchSessions();
|
||||
};
|
||||
|
||||
const handleUnlinkOIDC = async (userId: string, username: string) => {
|
||||
confirmWithToast(
|
||||
t("admin.unlinkOIDCDescription", { username }),
|
||||
async () => {
|
||||
try {
|
||||
const result = await unlinkOIDCFromPasswordAccount(userId);
|
||||
|
||||
toast.success(
|
||||
result.message || t("admin.unlinkOIDCSuccess", { username }),
|
||||
);
|
||||
fetchUsers();
|
||||
fetchSessions();
|
||||
} catch (error: unknown) {
|
||||
const err = error as {
|
||||
response?: { data?: { error?: string; code?: string } };
|
||||
};
|
||||
toast.error(
|
||||
err.response?.data?.error || t("admin.failedToUnlinkOIDC"),
|
||||
);
|
||||
}
|
||||
},
|
||||
"destructive",
|
||||
);
|
||||
};
|
||||
|
||||
const topMarginPx = isTopbarOpen ? 74 : 26;
|
||||
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
|
||||
const bottomMarginPx = 8;
|
||||
const wrapperStyle: React.CSSProperties = {
|
||||
marginLeft: leftMarginPx,
|
||||
marginRight: rightSidebarOpen
|
||||
? `calc(var(--right-sidebar-width, ${rightSidebarWidth}px) + 8px)`
|
||||
: 17,
|
||||
marginTop: topMarginPx,
|
||||
marginBottom: bottomMarginPx,
|
||||
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
|
||||
transition:
|
||||
"margin-left 200ms linear, margin-right 200ms linear, margin-top 200ms linear",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={wrapperStyle}
|
||||
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
|
||||
>
|
||||
<SimpleLoader visible={loading} message={t("common.loading")} />
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||||
<h1 className="font-bold text-lg">{t("admin.title")}</h1>
|
||||
</div>
|
||||
<Separator className="p-0.25 w-full" />
|
||||
|
||||
<div className="px-6 py-4 overflow-auto thin-scrollbar">
|
||||
<Tabs
|
||||
defaultValue="registration"
|
||||
onValueChange={(value) => {
|
||||
if (value === "users") {
|
||||
fetchUsers();
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="mb-4 bg-elevated border-2 border-edge">
|
||||
<TabsTrigger
|
||||
value="registration"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<Users className="h-4 w-4" />
|
||||
{t("admin.general")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="oidc"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
OIDC
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="users"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<Users className="h-4 w-4" />
|
||||
{t("admin.users")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="sessions"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
Sessions
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="roles"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
{t("rbac.roles.label")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="security"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<Database className="h-4 w-4" />
|
||||
{t("admin.databaseSecurity")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="api-keys"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<Key className="h-4 w-4" />
|
||||
{t("admin.apiKeys.tabLabel")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="registration" className="space-y-6">
|
||||
<GeneralSettingsTab
|
||||
allowRegistration={allowRegistration}
|
||||
setAllowRegistration={setAllowRegistration}
|
||||
allowPasswordLogin={allowPasswordLogin}
|
||||
setAllowPasswordLogin={setAllowPasswordLogin}
|
||||
allowPasswordReset={allowPasswordReset}
|
||||
setAllowPasswordReset={setAllowPasswordReset}
|
||||
oidcConfig={oidcConfig}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="oidc" className="space-y-6">
|
||||
<OIDCSettingsTab
|
||||
allowPasswordLogin={allowPasswordLogin}
|
||||
oidcConfig={oidcConfig}
|
||||
setOidcConfig={setOidcConfig}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="users" className="space-y-6">
|
||||
<UserManagementTab
|
||||
users={users}
|
||||
usersLoading={usersLoading}
|
||||
allowPasswordLogin={allowPasswordLogin}
|
||||
fetchUsers={fetchUsers}
|
||||
onCreateUser={() => setCreateUserDialogOpen(true)}
|
||||
onEditUser={handleEditUser}
|
||||
onLinkOIDCUser={handleLinkOIDCUser}
|
||||
onUnlinkOIDC={handleUnlinkOIDC}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="sessions" className="space-y-6">
|
||||
<SessionManagementTab
|
||||
sessions={sessions}
|
||||
sessionsLoading={sessionsLoading}
|
||||
fetchSessions={fetchSessions}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="roles" className="space-y-6">
|
||||
<div className="rounded-lg border-2 border-border bg-card p-4 space-y-4">
|
||||
<RolesTab />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="security" className="space-y-6">
|
||||
<DatabaseSecurityTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="api-keys" className="space-y-6">
|
||||
<ApiKeysTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateUserDialog
|
||||
open={createUserDialogOpen}
|
||||
onOpenChange={setCreateUserDialogOpen}
|
||||
onSuccess={handleCreateUserSuccess}
|
||||
/>
|
||||
|
||||
<UserEditDialog
|
||||
open={userEditDialogOpen}
|
||||
onOpenChange={setUserEditDialogOpen}
|
||||
user={selectedUserForEdit}
|
||||
currentUser={currentUser}
|
||||
onSuccess={handleEditUserSuccess}
|
||||
/>
|
||||
|
||||
<LinkAccountDialog
|
||||
open={linkAccountAlertOpen}
|
||||
onOpenChange={setLinkAccountAlertOpen}
|
||||
oidcUser={linkOidcUser}
|
||||
onSuccess={handleLinkSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminSettings;
|
||||
Reference in New Issue
Block a user