re-vibecode
This commit is contained in:
+190
-71
@@ -1,77 +1,196 @@
|
||||
import { useState } from "react";
|
||||
import type { ProfileSettings } from "@internal-mail/shared";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
import { MailboxPage } from "./pages/MailboxPage";
|
||||
import { SettingsPage } from "./pages/SettingsPage";
|
||||
import { useSession } from "./hooks/useSession";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { AdminSession, CreateUserResponse, MailUser } from "@internal-mail/shared";
|
||||
import { api } from "./api/client";
|
||||
import DashboardPage from "./pages/DashboardPage";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
||||
|
||||
type View = "inbox" | "compose" | "settings";
|
||||
const anonymousSession: AdminSession = {
|
||||
authenticated: false,
|
||||
adminEmail: null
|
||||
};
|
||||
|
||||
const AppShell = () => {
|
||||
const { session, logout } = useSession();
|
||||
const [currentView, setCurrentView] = useState<View>("inbox");
|
||||
const [profileOverride, setProfileOverride] = useState<ProfileSettings | null>(null);
|
||||
const getPath = (): string => window.location.pathname;
|
||||
|
||||
const user = profileOverride ?? session.user;
|
||||
export default function App() {
|
||||
const [path, setPath] = useState(getPath());
|
||||
const [session, setSession] = useState<AdminSession | null>(null);
|
||||
const [users, setUsers] = useState<MailUser[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [loginError, setLoginError] = useState<string | null>(null);
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopstate = () => setPath(getPath());
|
||||
window.addEventListener("popstate", handlePopstate);
|
||||
return () => window.removeEventListener("popstate", handlePopstate);
|
||||
}, []);
|
||||
|
||||
const resetToken = new URLSearchParams(window.location.search).get("token") ?? "";
|
||||
|
||||
const loadUsers = async () => {
|
||||
const response = await api.listUsers();
|
||||
setUsers(response.items);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (path.startsWith("/reset-password")) {
|
||||
return;
|
||||
}
|
||||
|
||||
api
|
||||
.getSession()
|
||||
.then((response) => {
|
||||
setSession(response.session);
|
||||
if (!response.session.authenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadUsers().catch((error) => {
|
||||
setErrorMessage(error instanceof Error ? error.message : "Could not load users.");
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setSession(anonymousSession);
|
||||
});
|
||||
}, [path]);
|
||||
|
||||
const handleLogin = async (payload: { email: string; password: string }) => {
|
||||
setBusy(true);
|
||||
setLoginError(null);
|
||||
|
||||
try {
|
||||
const response = await api.login(payload);
|
||||
setSession(response.session);
|
||||
setErrorMessage(null);
|
||||
await loadUsers().catch((error) => {
|
||||
setErrorMessage(error instanceof Error ? error.message : "Signed in, but could not load users.");
|
||||
});
|
||||
} catch (error) {
|
||||
setLoginError(error instanceof Error ? error.message : "Could not sign in.");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
const response = await api.logout();
|
||||
setSession(response.session);
|
||||
setUsers([]);
|
||||
setFeedback(null);
|
||||
setErrorMessage(null);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateUser = async (email: string): Promise<CreateUserResponse> => {
|
||||
setBusy(true);
|
||||
setFeedback(null);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const result = await api.createUser({ email });
|
||||
await loadUsers();
|
||||
return result;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePassword = async (email: string, password: string) => {
|
||||
setBusy(true);
|
||||
setFeedback(null);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
await api.updatePassword(email, { password });
|
||||
await loadUsers();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (email: string) => {
|
||||
setBusy(true);
|
||||
setFeedback(null);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
await api.deleteUser(email);
|
||||
await loadUsers();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTriggerReset = async (email: string) => {
|
||||
setBusy(true);
|
||||
setFeedback(null);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const result = await api.triggerPasswordReset(email);
|
||||
return result.expiresAt;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await loadUsers();
|
||||
setErrorMessage(null);
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : "Could not refresh users.");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (path.startsWith("/reset-password")) {
|
||||
return <ResetPasswordPage token={resetToken} />;
|
||||
}
|
||||
|
||||
if (session === null) {
|
||||
return (
|
||||
<main className="auth-shell">
|
||||
<section className="auth-card">
|
||||
<div className="eyebrow">Mail Admin</div>
|
||||
<h1>Loading</h1>
|
||||
<p className="auth-copy">Checking your admin session.</p>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session || !session.authenticated || !session.adminEmail) {
|
||||
return (
|
||||
<LoginPage
|
||||
busy={busy}
|
||||
error={loginError}
|
||||
onSubmit={handleLogin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<aside className="sidebar">
|
||||
<div className="brand-block">
|
||||
<div className="brand-block__overline">Zero-trust posture</div>
|
||||
<h1>Secure Webmail</h1>
|
||||
<p>Docker-managed mailboxes with a web layer designed to fail closed.</p>
|
||||
</div>
|
||||
|
||||
<nav className="nav-stack">
|
||||
<button type="button" className={currentView === "inbox" ? "nav-button nav-button--active" : "nav-button"} onClick={() => setCurrentView("inbox")}>
|
||||
Inbox
|
||||
</button>
|
||||
<button type="button" className={currentView === "compose" ? "nav-button nav-button--active" : "nav-button"} onClick={() => setCurrentView("compose")}>
|
||||
Compose
|
||||
</button>
|
||||
<button type="button" className={currentView === "settings" ? "nav-button nav-button--active" : "nav-button"} onClick={() => setCurrentView("settings")}>
|
||||
Settings
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div className="sidebar__footer">
|
||||
<div className="identity-card">
|
||||
{user?.avatarUrl ? <img src={user.avatarUrl} alt="" className="avatar-mini" /> : <div className="avatar-fallback">{user?.displayName.slice(0, 1) ?? "?"}</div>}
|
||||
<div>
|
||||
<strong>{user?.displayName}</strong>
|
||||
<p>{user?.email}</p>
|
||||
<small>{user?.totpEnabled ? "2FA enabled" : "2FA optional"}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" className="ghost-button" onClick={() => void logout()}>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="content-shell">
|
||||
{currentView === "settings" ? (
|
||||
<SettingsPage onProfileUpdate={(profile) => setProfileOverride(profile)} />
|
||||
) : (
|
||||
<MailboxPage currentView={currentView === "compose" ? "compose" : "inbox"} />
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
<DashboardPage
|
||||
adminEmail={session.adminEmail}
|
||||
users={users}
|
||||
busy={busy}
|
||||
feedback={feedback}
|
||||
errorMessage={errorMessage}
|
||||
onCreateUser={handleCreateUser}
|
||||
onUpdatePassword={handleUpdatePassword}
|
||||
onDeleteUser={handleDeleteUser}
|
||||
onTriggerReset={handleTriggerReset}
|
||||
onRefresh={handleRefresh}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const App = () => {
|
||||
const { loading, authBusy, error, login, session } = useSession();
|
||||
|
||||
if (loading) {
|
||||
return <main className="loading-screen">Loading secure session context...</main>;
|
||||
}
|
||||
|
||||
if (!session.authenticated) {
|
||||
return <LoginPage busy={authBusy} error={error} onLogin={login} />;
|
||||
}
|
||||
|
||||
return <AppShell />;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user