Make Cobble a local install: setup flow, bots-first admin, settings
Local, password-only auth: - No public pages; everything outside /admin and /auth redirects in - First run (no users) sends every page to /auth/signup, which creates the super admin (user #1) once and then locks - Login is email/username + password; removed the emailed-code flow, /auth/sso and email password reset - Google login kept for existing accounts only, never creates them - seed-super-admin now requires SEED_SUPER_ADMIN_PASSWORD Bots-first admin: - /admin and closed sections (people, logs, backups) redirect to /admin/bots; only bots and settings are live (AdminSections) - Removed the old admin layout; every /admin page uses the bots rail - Coordinator bot created at signup/seed and by /admin/bots (ensureCoordinatorBot), which redirects to its chat - bots.system_prompt (markdown) with a default coordinator prompt - seed-bots reuses the existing coordinator Settings: - Sub pages general (default), profile, change-password, listed in the rail with a back link; unknown settings paths redirect to general - Redesigned settings UI: pane header, grouped rows, sticky save bar - General: System/Light/Dark theme, saved per device Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import { AdminHomePath } from "@/src/dict/admin-sections-dict";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import type { ApiReqParams, GoogleWindow } from "@/src/types";
|
||||
@@ -8,20 +9,15 @@ import { useContext, useEffect, useRef, useState } from "react";
|
||||
const GoogleScriptSelector =
|
||||
'script[src="https://accounts.google.com/gsi/client"]';
|
||||
|
||||
export type GoogleAuthMode = "login" | "signup";
|
||||
|
||||
type Params = Pick<
|
||||
ReturnType<typeof useFormInit>,
|
||||
"loading" | "setLoading" | "setStatus"
|
||||
> & {
|
||||
mode: GoogleAuthMode;
|
||||
};
|
||||
>;
|
||||
|
||||
export default function useGoogleAuth({
|
||||
loading,
|
||||
setStatus,
|
||||
setLoading,
|
||||
mode,
|
||||
}: Params) {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
@@ -42,21 +38,16 @@ export default function useGoogleAuth({
|
||||
method: "POST",
|
||||
body: {
|
||||
google_token,
|
||||
google_signup: mode == "signup",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
window.location.pathname = `/admin`;
|
||||
window.location.pathname = AdminHomePath;
|
||||
} else {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg:
|
||||
res.msg ||
|
||||
(mode == "signup"
|
||||
? "Couldn't sign up with Google."
|
||||
: "Couldn't log in with Google."),
|
||||
msg: res.msg || "Couldn't log in with Google.",
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -175,7 +166,7 @@ export default function useGoogleAuth({
|
||||
googleId.renderButton(googleButtonRef.current, {
|
||||
theme: "outline",
|
||||
size: "large",
|
||||
text: mode == "signup" ? "signup_with" : "signin_with",
|
||||
text: "signin_with",
|
||||
shape: "rectangular",
|
||||
width: Math.min(buttonWidth, 400),
|
||||
});
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import type { LoginMethod } from "@/src/types";
|
||||
|
||||
/**
|
||||
* Site-wide login method from `AUTH_LOGIN_METHOD`, passed down by
|
||||
* `__root.server.ts`.
|
||||
*/
|
||||
export default function useLoginMethod(): LoginMethod {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
return pageProps?.envs?.AUTH_LOGIN_METHOD == "password" ? "password" : "code";
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import readSSOPendingLogin from "@/src/functions/frontend/read-sso-pending-login";
|
||||
import type { SSOPendingLogin } from "@/src/types";
|
||||
|
||||
export default function useSSOPendingLogin() {
|
||||
const [pending, setPending] = useState<SSOPendingLogin>();
|
||||
|
||||
useEffect(() => {
|
||||
setPending(readSSOPendingLogin());
|
||||
}, []);
|
||||
|
||||
return pending;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { ThemePreference } from "@/src/dict/theme-preferences-dict";
|
||||
import resolveTheme from "@/src/utils/resolve-theme";
|
||||
|
||||
const DarkQuery = "(prefers-color-scheme: dark)";
|
||||
|
||||
function readPreference(): ThemePreference {
|
||||
try {
|
||||
const stored = localStorage.getItem(AppData["ThemeStorageKey"]);
|
||||
return stored == "dark" || stored == "light" ? stored : "system";
|
||||
} catch {
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme({ preference }: { preference: ThemePreference }) {
|
||||
const theme = resolveTheme({
|
||||
preference,
|
||||
system_is_dark: window.matchMedia(DarkQuery).matches,
|
||||
});
|
||||
|
||||
document.documentElement.classList.remove("dark", "light");
|
||||
document.documentElement.classList.add(theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* This device's color scheme choice, stored where `ThemeInitScript` reads
|
||||
* it. `system` stores nothing and tracks the OS live.
|
||||
*/
|
||||
export default function useThemePreference() {
|
||||
const [preference, setPreferenceState] = useState<ThemePreference>();
|
||||
|
||||
useEffect(() => {
|
||||
setPreferenceState(readPreference());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (preference != "system") return;
|
||||
|
||||
const media = window.matchMedia(DarkQuery);
|
||||
const onChange = () => applyTheme({ preference: "system" });
|
||||
|
||||
media.addEventListener("change", onChange);
|
||||
return () => media.removeEventListener("change", onChange);
|
||||
}, [preference]);
|
||||
|
||||
const setPreference = (next: ThemePreference) => {
|
||||
try {
|
||||
if (next == "system") {
|
||||
localStorage.removeItem(AppData["ThemeStorageKey"]);
|
||||
} else {
|
||||
localStorage.setItem(AppData["ThemeStorageKey"], next);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
applyTheme({ preference: next });
|
||||
setPreferenceState(next);
|
||||
};
|
||||
|
||||
return { preference, setPreference };
|
||||
}
|
||||
Reference in New Issue
Block a user