Commit 705bdcba authored by Administrator's avatar Administrator

Update 5 files via Son of Anton

parent e3643210
This diff is collapsed.
""" """
Authentication routes: register, login, profile — with permissions + registration toggle. Authentication routes: register, login, profile — with permissions and registration toggle.
""" """
from pydantic import BaseModel from pydantic import BaseModel
...@@ -28,23 +28,22 @@ class LoginBody(BaseModel): ...@@ -28,23 +28,22 @@ class LoginBody(BaseModel):
password: str password: str
@router.get("/config") class ProfileOut(BaseModel):
def auth_config(db: Session = Depends(get_db)): pass
"""Public endpoint — no auth needed. Returns registration status."""
settings = db.query(AppSettings).first()
return { class Config(BaseModel):
"allow_registration": settings.allow_registration if settings else True, pass
}
@router.post("/register") @router.post("/register")
def register(body: RegisterBody, db: Session = Depends(get_db)): def register(body: RegisterBody, db: Session = Depends(get_db)):
# Check if registration is enabled # Check if registration is enabled
app_settings = db.query(AppSettings).first() settings = db.query(AppSettings).first()
if app_settings and not app_settings.allow_registration: if settings and not settings.allow_registration:
raise HTTPException( raise HTTPException(
status.HTTP_403_FORBIDDEN, status.HTTP_403_FORBIDDEN,
"Registration is currently disabled. Contact an administrator.", "Registration is currently disabled. Contact your administrator.",
) )
if db.query(User).filter( if db.query(User).filter(
...@@ -63,6 +62,7 @@ def register(body: RegisterBody, db: Session = Depends(get_db)): ...@@ -63,6 +62,7 @@ def register(body: RegisterBody, db: Session = Depends(get_db)):
db.commit() db.commit()
db.refresh(user) db.refresh(user)
# Auto-create permissions from defaults template
ensure_user_permissions(user.id, db) ensure_user_permissions(user.id, db)
token = create_token(user.id, user.role) token = create_token(user.id, user.role)
...@@ -88,6 +88,16 @@ def me(user: User = Depends(get_current_user), db: Session = Depends(get_db)): ...@@ -88,6 +88,16 @@ def me(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
return _user_dict(user, perms) return _user_dict(user, perms)
@router.get("/config")
def get_public_config(db: Session = Depends(get_db)):
"""Public endpoint — no auth required. Returns app config the login page needs."""
settings = db.query(AppSettings).first()
allow_reg = True
if settings:
allow_reg = settings.allow_registration
return {"allow_registration": allow_reg}
def _user_dict(u: User, perms: dict = None) -> dict: def _user_dict(u: User, perms: dict = None) -> dict:
d = { d = {
"id": u.id, "id": u.id,
......
// ── Registration toggle (append to end of file) ── const BASE = "/api";
export const getAuthConfig = () => function headers(token) {
fetch(`${BASE}/auth/config`).then((r) => r.json()); const h = { "Content-Type": "application/json" };
if (token) h["Authorization"] = `Bearer ${token}`;
return h;
}
export const getRegistrationSetting = (token) => function authHeader(token) {
request("GET", "/admin/registration", token); return token ? { Authorization: `Bearer ${token}` } : {};
}
export const toggleRegistration = (token) => async function request(method, path, token, body) {
request("PUT", "/admin/registration", token); const opts = { method, headers: headers(token) };
\ No newline at end of file if (body) opts.body = JSON.stringify(body);
const res = await fetch(`${BASE}${path}`, opts);
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(err.detail || err.message || "Request failed");
}
return res.json();
}
export const login = (username, password) =>
request("POST", "/auth/login", null, { username, password });
export const register = (username, email, password) =>
request("POST", "/auth/register", null, { username, email, password });
export const getMe = (token) => request("GET", "/auth/me", token);
export const getPublicConfig = () => request("GET", "/auth/config", null);
export const listChats = (token) => request("GET", "/chats", token);
export const createChat = (token, data = {}) => request("POST", "/chats", token, data);
export const updateChat = (token, chatId, data) =>
request("PUT", `/chats/${chatId}`, token, data);
export const renameChat = (token, chatId, title) =>
updateChat(token, chatId, { title });
export const deleteChat = (token, chatId) =>
request("DELETE", `/chats/${chatId}`, token);
export const getMessages = (token, chatId) =>
request("GET", `/chats/${chatId}/messages`, token);
export async function* streamMessage(token, chatId, body, signal) {
const res = await fetch(`${BASE}/chats/${chatId}/messages`, {
method: "POST", headers: headers(token),
body: JSON.stringify(body), signal,
});
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(err.detail || "Stream failed");
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() || "";
for (const part of parts) {
const line = part.trim();
if (line.startsWith("data: ")) {
try { yield JSON.parse(line.slice(6)); } catch { }
}
}
}
if (buffer.trim().startsWith("data: ")) {
try { yield JSON.parse(buffer.trim().slice(6)); } catch { }
}
}
export async function uploadAttachments(token, chatId, files) {
const form = new FormData();
for (const file of files) form.append("files", file);
const res = await fetch(`${BASE}/chats/${chatId}/attachments`, {
method: "POST", headers: authHeader(token), body: form,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || "Upload failed");
}
return res.json();
}
export function getAttachmentUrl(attachmentId) {
return `${BASE}/attachments/${attachmentId}/file`;
}
export const deleteAttachment = (token, attachmentId) =>
request("DELETE", `/attachments/${attachmentId}`, token);
export const listKnowledgeBases = (token) => request("GET", "/knowledge", token);
export const createKnowledgeBase = (token, name, description = "") =>
request("POST", "/knowledge", token, { name, description });
export const getKnowledgeBase = (token, kbId) =>
request("GET", `/knowledge/${kbId}`, token);
export const deleteKnowledgeBase = (token, kbId) =>
request("DELETE", `/knowledge/${kbId}`, token);
export async function uploadDocuments(token, kbId, files) {
const form = new FormData();
for (const file of files) form.append("files", file);
const res = await fetch(`${BASE}/knowledge/${kbId}/upload`, {
method: "POST", headers: authHeader(token), body: form,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || "Upload failed");
}
return res.json();
}
export const uploadDocument = (token, kbId, file) =>
uploadDocuments(token, kbId, [file]);
export const adminStats = (token) => request("GET", "/admin/stats", token);
export const adminListUsers = (token) => request("GET", "/admin/users", token);
export const adminCreateUser = (token, data) =>
request("POST", "/admin/users", token, data);
export const adminUpdateUser = (token, userId, data) =>
request("PUT", `/admin/users/${userId}`, token, data);
export const adminDeleteUser = (token, userId) =>
request("DELETE", `/admin/users/${userId}`, token);
export const adminListChats = (token) => request("GET", "/admin/chats", token);
export const adminGetSettings = (token) => request("GET", "/admin/settings", token);
export const adminUpdateSettings = (token, data) =>
request("PUT", "/admin/settings", token, data);
export async function downloadZip(token, markdown) {
const res = await fetch(`${BASE}/files/download-zip`, {
method: "POST", headers: headers(token),
body: JSON.stringify({ markdown }),
});
if (!res.ok) throw new Error("Download failed");
const ct = res.headers.get("content-type") || "";
if (ct.includes("application/zip")) {
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "son-of-anton-code.zip";
a.click();
URL.revokeObjectURL(url);
} else {
const data = await res.json();
if (data.error) throw new Error(data.error);
}
}
\ No newline at end of file
This diff is collapsed.
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useApp } from "../store"; import { useApp } from "../store";
import { login, register, getAuthConfig } from "../api"; import { login, register, getPublicConfig } from "../api";
import { Flame, LogIn, UserPlus, AlertCircle } from "lucide-react"; import { Flame, Eye, EyeOff, Loader2 } from "lucide-react";
export default function LoginPage() { export default function LoginPage() {
const { dispatch } = useApp(); const { dispatch } = useApp();
...@@ -9,21 +9,23 @@ export default function LoginPage() { ...@@ -9,21 +9,23 @@ export default function LoginPage() {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [showPw, setShowPw] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [allowRegistration, setAllowRegistration] = useState(true); const [allowRegistration, setAllowRegistration] = useState(true);
const [configLoaded, setConfigLoaded] = useState(false); const [configLoaded, setConfigLoaded] = useState(false);
useEffect(() => { useEffect(() => {
getAuthConfig() (async () => {
.then((data) => { try {
setAllowRegistration(data.allow_registration !== false); const cfg = await getPublicConfig();
setConfigLoaded(true); setAllowRegistration(cfg.allow_registration);
}) } catch {
.catch(() => { // If config fetch fails, default to allowing registration
setAllowRegistration(true); setAllowRegistration(true);
}
setConfigLoaded(true); setConfigLoaded(true);
}); })();
}, []); }, []);
async function handleSubmit(e) { async function handleSubmit(e) {
...@@ -31,26 +33,31 @@ export default function LoginPage() { ...@@ -31,26 +33,31 @@ export default function LoginPage() {
setError(""); setError("");
setLoading(true); setLoading(true);
try { try {
let result; let res;
if (isRegister) { if (isRegister) {
if (!allowRegistration) { if (!email) { setError("Email is required"); setLoading(false); return; }
setError("Registration is disabled."); res = await register(username, email, password);
setLoading(false);
return;
}
result = await register(username, email, password);
} else { } else {
result = await login(username, password); res = await login(username, password);
} }
dispatch({ type: "LOGIN", token: result.token, user: result.user }); dispatch({ type: "SET_TOKEN", token: res.token });
dispatch({ type: "SET_USER", user: res.user });
} catch (err) { } catch (err) {
setError(err.message || "Something went wrong"); setError(err.message);
} }
setLoading(false); setLoading(false);
} }
if (!configLoaded) {
return ( return (
<div className="h-dvh flex items-center justify-center bg-anton-bg p-4"> <div className="h-dvh flex items-center justify-center bg-anton-bg">
<Loader2 className="animate-spin text-anton-accent" size={32} />
</div>
);
}
return (
<div className="h-dvh flex items-center justify-center bg-anton-bg px-4">
<div className="w-full max-w-sm"> <div className="w-full max-w-sm">
<div className="flex flex-col items-center mb-8"> <div className="flex flex-col items-center mb-8">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20 mb-4"> <div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20 mb-4">
...@@ -60,85 +67,65 @@ export default function LoginPage() { ...@@ -60,85 +67,65 @@ export default function LoginPage() {
<p className="text-anton-muted text-sm mt-1">Avatar of All Elements of Code</p> <p className="text-anton-muted text-sm mt-1">Avatar of All Elements of Code</p>
</div> </div>
<div className="bg-anton-card border border-anton-border rounded-2xl p-6"> <form onSubmit={handleSubmit} className="bg-anton-card border border-anton-border rounded-2xl p-6 space-y-4">
{/* Tab buttons — only show Register tab if registration is allowed */} <h2 className="text-lg font-semibold text-white text-center">
<div className="flex gap-2 mb-6"> {isRegister ? "Create Account" : "Sign In"}
<button </h2>
onClick={() => { setIsRegister(false); setError(""); }}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${
!isRegister ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"
}`}
>
<LogIn size={14} className="inline mr-1.5" />
Login
</button>
{allowRegistration && (
<button
onClick={() => { setIsRegister(true); setError(""); }}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${
isRegister ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"
}`}
>
<UserPlus size={14} className="inline mr-1.5" />
Register
</button>
)}
</div>
{error && ( {error && (
<div className="mb-4 bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 flex items-start gap-2"> <div className="bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 text-red-400 text-sm">
<AlertCircle size={14} className="text-red-400 mt-0.5 shrink-0" /> {error}
<span className="text-red-400 text-xs">{error}</span>
</div> </div>
)} )}
<form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="text-xs text-anton-muted mb-1 block">Username</label> <label className="text-xs text-anton-muted mb-1 block">Username</label>
<input <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} required autoFocus
type="text" value={username} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent" />
onChange={(e) => setUsername(e.target.value)}
className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent transition"
required autoComplete="username"
/>
</div> </div>
{isRegister && ( {isRegister && (
<div> <div>
<label className="text-xs text-anton-muted mb-1 block">Email</label> <label className="text-xs text-anton-muted mb-1 block">Email</label>
<input <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
type="email" value={email} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent" />
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent transition"
required autoComplete="email"
/>
</div> </div>
)} )}
<div> <div>
<label className="text-xs text-anton-muted mb-1 block">Password</label> <label className="text-xs text-anton-muted mb-1 block">Password</label>
<input <div className="relative">
type="password" value={password} <input type={showPw ? "text" : "password"} value={password} onChange={(e) => setPassword(e.target.value)} required
onChange={(e) => setPassword(e.target.value)} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent pr-10" />
className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent transition" <button type="button" onClick={() => setShowPw(!showPw)}
required autoComplete={isRegister ? "new-password" : "current-password"} className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white">
/> {showPw ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div> </div>
<button <button type="submit" disabled={loading}
type="submit" disabled={loading} className="w-full bg-anton-accent text-white rounded-lg py-2.5 text-sm font-medium hover:opacity-90 transition disabled:opacity-50 flex items-center justify-center gap-2">
className="w-full bg-anton-accent text-white py-2.5 rounded-lg font-medium hover:opacity-90 transition disabled:opacity-50" {loading && <Loader2 size={16} className="animate-spin" />}
> {isRegister ? "Create Account" : "Sign In"}
{loading ? "..." : isRegister ? "Create Account" : "Sign In"}
</button> </button>
</form>
{!allowRegistration && configLoaded && !isRegister && ( {allowRegistration && (
<p className="text-[11px] text-anton-muted text-center mt-4"> <p className="text-center text-sm text-anton-muted">
Registration is currently disabled by the administrator. {isRegister ? "Already have an account?" : "Don't have an account?"}{" "}
<button type="button" onClick={() => { setIsRegister(!isRegister); setError(""); }}
className="text-anton-accent hover:underline">
{isRegister ? "Sign In" : "Register"}
</button>
</p>
)}
{!allowRegistration && !isRegister && (
<p className="text-center text-xs text-anton-muted">
Registration is disabled. Contact your administrator.
</p> </p>
)} )}
</div> </form>
</div> </div>
</div> </div>
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment