feat: add system configuration API endpoint and frontend hook for dynamic settings

This commit is contained in:
2025-12-25 23:41:00 +01:00
parent 529457c9a3
commit 4cb398869d
6 changed files with 64 additions and 8 deletions

View File

@@ -0,0 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
export interface ConfigResponse {
allow_registration: boolean;
}
export function useConfig() {
return useQuery<ConfigResponse>({
queryKey: ["config"],
queryFn: () => api.get("/config"),
staleTime: Infinity, // Config rarely changes
});
}

View File

@@ -6,6 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useLogin } from "@/hooks/use-auth"; import { useLogin } from "@/hooks/use-auth";
import { useConfig } from "@/hooks/useConfig";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
@@ -22,6 +23,7 @@ type LoginFormValues = z.infer<typeof loginSchema>;
export default function LoginPage() { export default function LoginPage() {
const { mutate: login, isPending } = useLogin(); const { mutate: login, isPending } = useLogin();
const { data: config } = useConfig();
const form = useForm<LoginFormValues>({ const form = useForm<LoginFormValues>({
resolver: zodResolver(loginSchema), resolver: zodResolver(loginSchema),
@@ -95,12 +97,14 @@ export default function LoginPage() {
</Form> </Form>
</CardContent> </CardContent>
<CardFooter className="flex justify-center"> <CardFooter className="flex justify-center">
{config?.allow_registration !== false && (
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Don't have an account?{" "} Don't have an account?{" "}
<Link to="/register" className="font-semibold text-primary hover:underline"> <Link to="/register" className="font-semibold text-primary hover:underline">
Sign up Sign up
</Link> </Link>
</p> </p>
)}
</CardFooter> </CardFooter>
</Card> </Card>
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} dataManagementEnabled={false} /> <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} dataManagementEnabled={false} />

View File

@@ -1,11 +1,12 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
import { SettingsDialog } from "@/components/settings-dialog"; import { SettingsDialog } from "@/components/settings-dialog";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { Link } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useRegister } from "@/hooks/use-auth"; import { useRegister } from "@/hooks/use-auth";
import { useConfig } from "@/hooks/useConfig";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
@@ -26,6 +27,19 @@ type RegisterFormValues = z.infer<typeof registerSchema>;
export default function RegisterPage() { export default function RegisterPage() {
const { mutate: register, isPending } = useRegister(); const { mutate: register, isPending } = useRegister();
const { data: config, isLoading: isConfigLoading } = useConfig();
const navigate = useNavigate();
useEffect(() => {
if (!isConfigLoading && config?.allow_registration === false) {
toast.error("Registration is currently disabled");
navigate("/login");
}
}, [config, isConfigLoading, navigate]);
if (isConfigLoading || config?.allow_registration === false) {
return null; // Or a loading spinner
}
const form = useForm<RegisterFormValues>({ const form = useForm<RegisterFormValues>({
resolver: zodResolver(registerSchema), resolver: zodResolver(registerSchema),

View File

@@ -166,3 +166,9 @@ impl From<notes_domain::NoteVersion> for NoteVersionResponse {
} }
} }
} }
/// System configuration response
#[derive(Debug, Serialize)]
pub struct ConfigResponse {
pub allow_registration: bool,
}

View File

@@ -0,0 +1,14 @@
//! Configuration routes
use axum::{Json, extract::State};
use crate::dto::ConfigResponse;
use crate::error::ApiResult;
use crate::state::AppState;
/// Get system configuration
pub async fn get_config(State(state): State<AppState>) -> ApiResult<Json<ConfigResponse>> {
Ok(Json(ConfigResponse {
allow_registration: state.config.allow_registration,
}))
}

View File

@@ -1,6 +1,7 @@
//! Route definitions and module structure //! Route definitions and module structure
pub mod auth; pub mod auth;
pub mod config;
pub mod import_export; pub mod import_export;
pub mod notes; pub mod notes;
pub mod tags; pub mod tags;
@@ -40,4 +41,6 @@ pub fn api_v1_router() -> Router<AppState> {
"/tags/{id}", "/tags/{id}",
delete(tags::delete_tag).patch(tags::rename_tag), delete(tags::delete_tag).patch(tags::rename_tag),
) )
// System Config
.route("/config", get(config::get_config))
} }