diff --git a/frontend/src/components/Icon.vue b/frontend/src/components/Icon.vue index 94c6f9e..ca8718d 100644 --- a/frontend/src/components/Icon.vue +++ b/frontend/src/components/Icon.vue @@ -8,6 +8,7 @@ const paths: Record = { trash: '', restore: '', logout: '', + settings: '', }; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index ca1cc53..f82ace2 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,5 +1,6 @@ import { createRouter, createWebHistory } from "vue-router"; import { useSessionStore } from "../stores/session"; +import { useConfigStore } from "../stores/config"; const router = createRouter({ history: createWebHistory(), @@ -22,6 +23,12 @@ const router = createRouter({ component: () => import("../views/BoardView.vue"), meta: { requiresAuth: true }, }, + { + path: "/settings", + name: "settings", + component: () => import("../views/SettingsView.vue"), + meta: { requiresAuth: true, requiresAdmin: true }, + }, { path: "/login", name: "login", @@ -39,13 +46,22 @@ const router = createRouter({ router.beforeEach(async (to) => { const session = useSessionStore(); - // Resolve the current user once, lazily, before the first guarded navigation. + const config = useConfigStore(); + await config.load(); if (!session.loaded) { await session.fetchMe(); } if (to.meta.requiresAuth && !session.user) { return { name: "login", query: to.fullPath !== "/" ? { redirect: to.fullPath } : undefined }; } + if (to.meta.requiresAdmin && !session.user?.is_admin) { + return { name: "board" }; + } + // Registration closed: keep people out of the register screen (the very first + // account is still creatable because allow_registration defaults to true). + if (to.name === "register" && !config.allowRegistration) { + return { name: "login" }; + } if (to.meta.guestOnly && session.user) { return { name: "board" }; } diff --git a/frontend/src/stores/config.ts b/frontend/src/stores/config.ts new file mode 100644 index 0000000..210a300 --- /dev/null +++ b/frontend/src/stores/config.ts @@ -0,0 +1,38 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { api } from "../api/client"; + +interface PublicConfig { + site_name: string; + allow_registration: boolean; + version: string; +} + +// Public, unauthenticated app config (site name, whether signups are open). +export const useConfigStore = defineStore("config", () => { + const siteName = ref("ThoughtSync"); + const allowRegistration = ref(true); + const version = ref(""); + const loaded = ref(false); + + async function load(): Promise { + if (loaded.value) return; + try { + const cfg = await api.get("/api/config"); + siteName.value = cfg.site_name; + allowRegistration.value = cfg.allow_registration; + version.value = cfg.version; + } catch { + // Keep defaults if the config endpoint is unreachable. + } finally { + loaded.value = true; + } + } + + async function reload(): Promise { + loaded.value = false; + await load(); + } + + return { siteName, allowRegistration, version, loaded, load, reload }; +}); diff --git a/frontend/src/stores/session.ts b/frontend/src/stores/session.ts index 8ea89b0..4a21b0d 100644 --- a/frontend/src/stores/session.ts +++ b/frontend/src/stores/session.ts @@ -7,6 +7,7 @@ export interface User { email: string; display_name: string; email_verified: boolean; + is_admin: boolean; } export const useSessionStore = defineStore("session", () => { diff --git a/frontend/src/views/BoardView.vue b/frontend/src/views/BoardView.vue index f65c892..df1f4cd 100644 --- a/frontend/src/views/BoardView.vue +++ b/frontend/src/views/BoardView.vue @@ -2,6 +2,7 @@ import { computed, onMounted, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { useSessionStore } from "../stores/session"; +import { useConfigStore } from "../stores/config"; import { useNotesStore, type Note, type NoteView } from "../stores/notes"; import QuickAdd from "../components/QuickAdd.vue"; import NoteCard from "../components/NoteCard.vue"; @@ -10,6 +11,7 @@ import BaseButton from "../components/BaseButton.vue"; import Icon from "../components/Icon.vue"; const session = useSessionStore(); +const config = useConfigStore(); const notes = useNotesStore(); const route = useRoute(); const router = useRouter(); @@ -69,7 +71,7 @@ async function signOut() {
TS
- +
+ + + diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 493a4b3..60f9697 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -2,11 +2,13 @@ import { ref } from "vue"; import { useRouter, useRoute } from "vue-router"; import { useSessionStore } from "../stores/session"; +import { useConfigStore } from "../stores/config"; import BaseInput from "../components/BaseInput.vue"; import BaseButton from "../components/BaseButton.vue"; import type { ApiError } from "../api/client"; const session = useSessionStore(); +const config = useConfigStore(); const router = useRouter(); const route = useRoute(); @@ -72,7 +74,7 @@ async function submit() { Sign in -

+

No account? Create one +import { computed, onMounted, ref } from "vue"; +import { api } from "../api/client"; +import { useConfigStore } from "../stores/config"; +import BaseButton from "../components/BaseButton.vue"; + +interface SettingItem { + key: string; + type: "string" | "bool" | "int"; + value: string | boolean | number; + default: string | boolean | number; + label: string; + description: string; + group: string; +} + +const config = useConfigStore(); + +const items = ref([]); +const loading = ref(true); +const saving = ref(false); +const error = ref(""); +const saved = ref(false); + +const groups = computed(() => { + const map = new Map(); + for (const it of items.value) { + const arr = map.get(it.group) ?? []; + arr.push(it); + map.set(it.group, arr); + } + return Array.from(map, ([name, entries]) => ({ name, entries })); +}); + +async function load() { + loading.value = true; + try { + const res = await api.get<{ settings: SettingItem[] }>("/api/settings"); + items.value = res.settings; + } catch { + error.value = "Could not load settings."; + } finally { + loading.value = false; + } +} + +async function save() { + error.value = ""; + saved.value = false; + saving.value = true; + const payload: Record = {}; + for (const it of items.value) payload[it.key] = it.value; + try { + const res = await api.patch<{ settings: SettingItem[] }>("/api/settings", { settings: payload }); + items.value = res.settings; + saved.value = true; + await config.reload(); // header/site name reflects changes immediately + } catch (e) { + error.value = (e as { error?: string }).error ?? "Could not save settings."; + } finally { + saving.value = false; + } +} + +onMounted(load); + + +