Commit 91ada25a authored by Administrator's avatar Administrator

Update 6 files via Son of Anton

parent 3c51ab07
...@@ -48,12 +48,29 @@ def _run_migrations(): ...@@ -48,12 +48,29 @@ def _run_migrations():
from backend.models import ChatAttachment from backend.models import ChatAttachment
ChatAttachment.__table__.create(bind=engine, checkfirst=True) ChatAttachment.__table__.create(bind=engine, checkfirst=True)
# Create user_permissions table if missing
if "user_permissions" not in existing_tables: if "user_permissions" not in existing_tables:
from backend.models import UserPermissions from backend.models import UserPermissions
UserPermissions.__table__.create(bind=engine, checkfirst=True) UserPermissions.__table__.create(bind=engine, checkfirst=True)
print(" Created user_permissions table") print(" Created user_permissions table")
# ── App Settings table ──
if "app_settings" not in existing_tables:
from backend.models import AppSettings
AppSettings.__table__.create(bind=engine, checkfirst=True)
print(" Created app_settings table")
# Ensure default app_settings row exists
from backend.models import AppSettings
from backend.database import SessionLocal
_db = SessionLocal()
try:
if not _db.query(AppSettings).first():
_db.add(AppSettings(allow_registration=True))
_db.commit()
print(" Created default app_settings row")
finally:
_db.close()
for table_name in ["gitlab_settings", "linked_repos", "pending_actions"]: for table_name in ["gitlab_settings", "linked_repos", "pending_actions"]:
if table_name not in existing_tables: if table_name not in existing_tables:
print(f" Creating {table_name} table") print(f" Creating {table_name} table")
......
...@@ -54,7 +54,6 @@ class UserPermissions(Base): ...@@ -54,7 +54,6 @@ class UserPermissions(Base):
unique=True, nullable=False, index=True, unique=True, nullable=False, index=True,
) )
# Feature access
can_use_web_search = Column(Boolean, default=False) can_use_web_search = Column(Boolean, default=False)
can_use_ui_design = Column(Boolean, default=False) can_use_ui_design = Column(Boolean, default=False)
can_use_knowledge_base = Column(Boolean, default=True) can_use_knowledge_base = Column(Boolean, default=True)
...@@ -63,10 +62,8 @@ class UserPermissions(Base): ...@@ -63,10 +62,8 @@ class UserPermissions(Base):
can_export_pptx = Column(Boolean, default=True) can_export_pptx = Column(Boolean, default=True)
can_export_docx = Column(Boolean, default=True) can_export_docx = Column(Boolean, default=True)
# Model access — "all" or comma-separated model IDs
allowed_models = Column(Text, default="eu.anthropic.claude-haiku-4-5-20251001-v1:0") allowed_models = Column(Text, default="eu.anthropic.claude-haiku-4-5-20251001-v1:0")
# Limits (0 = unlimited for count-based limits)
max_tokens_cap = Column(Integer, default=4096) max_tokens_cap = Column(Integer, default=4096)
max_reasoning_budget = Column(Integer, default=0) max_reasoning_budget = Column(Integer, default=0)
max_chats = Column(Integer, default=50) max_chats = Column(Integer, default=50)
...@@ -81,6 +78,18 @@ class UserPermissions(Base): ...@@ -81,6 +78,18 @@ class UserPermissions(Base):
user = relationship("User", back_populates="permissions") user = relationship("User", back_populates="permissions")
# ═══════════════════════════════════════════════════════════
# App-wide Settings (singleton row)
# ═══════════════════════════════════════════════════════════
class AppSettings(Base):
__tablename__ = "app_settings"
id = Column(String(36), primary_key=True, default=new_id)
allow_registration = Column(Boolean, default=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Chat(Base): class Chat(Base):
__tablename__ = "chats" __tablename__ = "chats"
......
This diff is collapsed.
""" """
Authentication routes: register, login, profile — with permissions. Authentication routes: register, login, profile — with permissions + registration toggle.
""" """
from pydantic import BaseModel from pydantic import BaseModel
...@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status ...@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from backend.database import get_db from backend.database import get_db
from backend.models import User from backend.models import User, AppSettings
from backend.auth import ( from backend.auth import (
hash_password, verify_password, create_token, get_current_user, hash_password, verify_password, create_token, get_current_user,
get_user_permissions, ensure_user_permissions, get_user_permissions, ensure_user_permissions,
...@@ -28,8 +28,25 @@ class LoginBody(BaseModel): ...@@ -28,8 +28,25 @@ class LoginBody(BaseModel):
password: str password: str
@router.get("/config")
def auth_config(db: Session = Depends(get_db)):
"""Public endpoint — no auth needed. Returns registration status."""
settings = db.query(AppSettings).first()
return {
"allow_registration": settings.allow_registration if settings else True,
}
@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
app_settings = db.query(AppSettings).first()
if app_settings and not app_settings.allow_registration:
raise HTTPException(
status.HTTP_403_FORBIDDEN,
"Registration is currently disabled. Contact an administrator.",
)
if db.query(User).filter( if db.query(User).filter(
(User.username == body.username) | (User.email == body.email) (User.username == body.username) | (User.email == body.email)
).first(): ).first():
...@@ -46,7 +63,6 @@ def register(body: RegisterBody, db: Session = Depends(get_db)): ...@@ -46,7 +63,6 @@ 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)
......
This diff is collapsed.
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useApp } from "../store"; import { useApp } from "../store";
import { login, register } from "../api"; import { login, register, getAuthConfig } from "../api";
import { Flame, Eye, EyeOff, Loader2 } from "lucide-react"; import { Flame, LogIn, UserPlus, AlertCircle } from "lucide-react";
export default function LoginPage() { export default function LoginPage() {
const { dispatch } = useApp(); const { dispatch } = useApp();
...@@ -9,114 +9,136 @@ export default function LoginPage() { ...@@ -9,114 +9,136 @@ 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 [configLoaded, setConfigLoaded] = useState(false);
useEffect(() => {
getAuthConfig()
.then((data) => {
setAllowRegistration(data.allow_registration !== false);
setConfigLoaded(true);
})
.catch(() => {
setAllowRegistration(true);
setConfigLoaded(true);
});
}, []);
async function handleSubmit(e) { async function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
setError(""); setError("");
setLoading(true); setLoading(true);
try { try {
const res = isRegister let result;
? await register(username, email, password) if (isRegister) {
: await login(username, password); if (!allowRegistration) {
dispatch({ type: "LOGIN", token: res.token, user: res.user }); setError("Registration is disabled.");
} catch (err) {
setError(err.message || "Authentication failed");
} finally {
setLoading(false); setLoading(false);
return;
} }
result = await register(username, email, password);
} else {
result = await login(username, password);
}
dispatch({ type: "LOGIN", token: result.token, user: result.user });
} catch (err) {
setError(err.message || "Something went wrong");
}
setLoading(false);
} }
return ( return (
<div className="h-full h-dvh flex items-center justify-center bg-anton-bg px-4 safe-top safe-bottom"> <div className="h-dvh flex items-center justify-center bg-anton-bg p-4">
<div className="w-full max-w-sm"> <div className="w-full max-w-sm">
{/* Logo */} <div className="flex flex-col items-center mb-8">
<div className="text-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 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
<Flame size={32} className="text-white" /> <Flame size={32} className="text-white" />
</div> </div>
<h1 className="text-2xl font-bold text-white">Son of Anton</h1> <h1 className="text-2xl font-bold text-white">Son of Anton</h1>
<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>
{/* Form */} <div className="bg-anton-card border border-anton-border rounded-2xl p-6">
{/* Tab buttons — only show Register tab if registration is allowed */}
<div className="flex gap-2 mb-6">
<button
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 && (
<div className="mb-4 bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 flex items-start gap-2">
<AlertCircle size={14} className="text-red-400 mt-0.5 shrink-0" />
<span className="text-red-400 text-xs">{error}</span>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="text-xs text-anton-muted mb-1.5 block">Username</label> <label className="text-xs text-anton-muted mb-1 block">Username</label>
<input <input
type="text" type="text" value={username}
value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 text-white focus:outline-none focus:border-anton-accent transition" 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"
placeholder="Enter username" required autoComplete="username"
required
autoComplete="username"
autoCapitalize="off"
/> />
</div> </div>
{isRegister && ( {isRegister && (
<div> <div>
<label className="text-xs text-anton-muted mb-1.5 block">Email</label> <label className="text-xs text-anton-muted mb-1 block">Email</label>
<input <input
type="email" type="email" value={email}
value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 text-white focus:outline-none focus:border-anton-accent transition" 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"
placeholder="your@email.com" required autoComplete="email"
required
autoComplete="email"
/> />
</div> </div>
)} )}
<div> <div>
<label className="text-xs text-anton-muted mb-1.5 block">Password</label> <label className="text-xs text-anton-muted mb-1 block">Password</label>
<div className="relative">
<input <input
type={showPw ? "text" : "password"} type="password" value={password}
value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 pr-12 text-white focus:outline-none focus:border-anton-accent transition" 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"
placeholder="••••••••" required autoComplete={isRegister ? "new-password" : "current-password"}
required
autoComplete={isRegister ? "new-password" : "current-password"}
/> />
<button
type="button"
onClick={() => setShowPw(!showPw)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition p-1"
>
{showPw ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div> </div>
{error && (
<div className="bg-anton-danger/10 border border-anton-danger/30 text-anton-danger text-sm rounded-lg px-3 py-2.5">
{error}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full py-3.5 bg-anton-accent text-white rounded-xl font-semibold hover:opacity-90 transition disabled:opacity-50 active:scale-[0.98] flex items-center justify-center gap-2"
>
{loading && <Loader2 size={18} className="animate-spin" />}
{isRegister ? "Create Account" : "Sign In"}
</button>
<button <button
type="button" type="submit" disabled={loading}
onClick={() => { setIsRegister(!isRegister); setError(""); }} className="w-full bg-anton-accent text-white py-2.5 rounded-lg font-medium hover:opacity-90 transition disabled:opacity-50"
className="w-full text-center text-sm text-anton-muted hover:text-white transition py-2"
> >
{isRegister ? "Already have an account? Sign in" : "Need an account? Register"} {loading ? "..." : isRegister ? "Create Account" : "Sign In"}
</button> </button>
</form> </form>
{!allowRegistration && configLoaded && !isRegister && (
<p className="text-[11px] text-anton-muted text-center mt-4">
Registration is currently disabled by the administrator.
</p>
)}
</div>
</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