Commit 49192084 authored by Administrator's avatar Administrator

Update 9 files via Son of Anton

parent f96d6514
......@@ -58,6 +58,11 @@ CHROMADB_PATH = os.getenv("CHROMADB_PATH", "/data/chromadb")
# ═══════════════════════════════════════════════════
SERPAPI_KEY = os.getenv("SERPAPI_KEY", "")
# ═══════════════════════════════════════════════════
# App-level defaults
# ═══════════════════════════════════════════════════
REGISTRATION_ENABLED_DEFAULT = os.getenv("REGISTRATION_ENABLED", "true").lower() in ("true", "1", "yes")
# ═══════════════════════════════════════════════════
# PERMISSION DEFAULTS — applied to new regular users
# ═══════════════════════════════════════════════════
......
......@@ -48,12 +48,16 @@ def _run_migrations():
from backend.models import ChatAttachment
ChatAttachment.__table__.create(bind=engine, checkfirst=True)
# Create user_permissions table if missing
if "user_permissions" not in existing_tables:
from backend.models import UserPermissions
UserPermissions.__table__.create(bind=engine, checkfirst=True)
print(" Created user_permissions 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")
for table_name in ["gitlab_settings", "linked_repos", "pending_actions"]:
if table_name not in existing_tables:
print(f" Creating {table_name} table")
......
......@@ -54,7 +54,6 @@ class UserPermissions(Base):
unique=True, nullable=False, index=True,
)
# Feature access
can_use_web_search = Column(Boolean, default=False)
can_use_ui_design = Column(Boolean, default=False)
can_use_knowledge_base = Column(Boolean, default=True)
......@@ -63,10 +62,8 @@ class UserPermissions(Base):
can_export_pptx = 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")
# Limits (0 = unlimited for count-based limits)
max_tokens_cap = Column(Integer, default=4096)
max_reasoning_budget = Column(Integer, default=0)
max_chats = Column(Integer, default=50)
......@@ -81,6 +78,14 @@ class UserPermissions(Base):
user = relationship("User", back_populates="permissions")
class AppSettings(Base):
__tablename__ = "app_settings"
id = Column(String(36), primary_key=True, default=new_id)
registration_enabled = Column(Boolean, default=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Chat(Base):
__tablename__ = "chats"
......
"""
Superadmin routes: user management, stats, permissions — v4.2.0
Superadmin routes: user management, stats, permissions, app settings — v4.2.0
"""
from pydantic import BaseModel
......@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
from sqlalchemy import func
from backend.database import get_db
from backend.models import User, Chat, Message, KnowledgeBase, UserPermissions
from backend.models import User, Chat, Message, KnowledgeBase, UserPermissions, AppSettings
from backend.auth import (
require_superadmin, hash_password, get_user_permissions,
ensure_user_permissions, get_default_permissions_template,
......@@ -55,6 +55,10 @@ class PermissionsBody(BaseModel):
max_attachments_per_message: Optional[int] = None
class AppSettingsBody(BaseModel):
registration_enabled: Optional[bool] = None
# ═══════════════════════════════════════════════════
# Stats & Users
# ═══════════════════════════════════════════════════
......@@ -107,7 +111,6 @@ def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin),
db.add(user)
db.commit()
db.refresh(user)
# Auto-create permissions from defaults template
ensure_user_permissions(user.id, db)
return {"id": user.id, "username": user.username}
......@@ -160,6 +163,37 @@ def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depe
return result
# ═══════════════════════════════════════════════════
# APP SETTINGS (registration toggle, etc.)
# ═══════════════════════════════════════════════════
@router.get("/app-settings")
def get_app_settings(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
settings = db.query(AppSettings).first()
if not settings:
return {"registration_enabled": True}
return {
"registration_enabled": settings.registration_enabled,
"updated_at": str(settings.updated_at) if settings.updated_at else None,
}
@router.put("/app-settings")
def update_app_settings(body: AppSettingsBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
settings = db.query(AppSettings).first()
if not settings:
settings = AppSettings()
db.add(settings)
if body.registration_enabled is not None:
settings.registration_enabled = body.registration_enabled
db.commit()
db.refresh(settings)
return {
"registration_enabled": settings.registration_enabled,
"updated_at": str(settings.updated_at) if settings.updated_at else None,
}
# ═══════════════════════════════════════════════════
# PERMISSIONS MANAGEMENT
# ═══════════════════════════════════════════════════
......
"""
Authentication routes: register, login, profile — with permissions.
Authentication routes: register, login, profile — with permissions and registration toggle.
"""
from pydantic import BaseModel
......@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from backend.database import get_db
from backend.models import User
from backend.models import User, AppSettings
from backend.auth import (
hash_password, verify_password, create_token, get_current_user,
get_user_permissions, ensure_user_permissions,
......@@ -28,8 +28,25 @@ class LoginBody(BaseModel):
password: str
@router.get("/registration-status")
def registration_status(db: Session = Depends(get_db)):
"""Public endpoint — no auth required. Frontend checks this to show/hide register form."""
settings = db.query(AppSettings).first()
enabled = settings.registration_enabled if settings else config.REGISTRATION_ENABLED_DEFAULT
return {"registration_enabled": enabled}
@router.post("/register")
def register(body: RegisterBody, db: Session = Depends(get_db)):
# Check if registration is enabled
settings = db.query(AppSettings).first()
enabled = settings.registration_enabled if settings else config.REGISTRATION_ENABLED_DEFAULT
if not enabled:
raise HTTPException(
status.HTTP_403_FORBIDDEN,
"Registration is currently disabled. Contact your administrator.",
)
if db.query(User).filter(
(User.username == body.username) | (User.email == body.email)
).first():
......@@ -46,7 +63,6 @@ def register(body: RegisterBody, db: Session = Depends(get_db)):
db.commit()
db.refresh(user)
# Auto-create permissions from defaults template
ensure_user_permissions(user.id, db)
token = create_token(user.id, user.role)
......
"""
Seed superadmin user and default permissions template.
Seed superadmin user, default permissions template, and app settings.
"""
from backend.database import SessionLocal
from backend.models import User, UserPermissions
from backend.models import User, UserPermissions, AppSettings
from backend.auth import hash_password
from backend.config import SUPERADMIN_PASSWORD, SUPERADMIN_PERMISSIONS, DEFAULT_PERMISSIONS, PERMISSION_FIELDS
from backend.config import (
SUPERADMIN_PASSWORD, SUPERADMIN_PERMISSIONS, DEFAULT_PERMISSIONS,
PERMISSION_FIELDS, REGISTRATION_ENABLED_DEFAULT,
)
def seed_superadmin():
......@@ -25,7 +28,6 @@ def seed_superadmin():
db.refresh(user)
print(f" Created superadmin (password: {SUPERADMIN_PASSWORD})")
# Create superadmin permissions
perms = UserPermissions(user_id=user.id)
for field in PERMISSION_FIELDS:
if hasattr(perms, field):
......@@ -34,7 +36,6 @@ def seed_superadmin():
db.commit()
print(" Created superadmin permissions")
else:
# Ensure superadmin has permissions row
sp = db.query(UserPermissions).filter(UserPermissions.user_id == existing.id).first()
if not sp:
perms = UserPermissions(user_id=existing.id)
......@@ -45,7 +46,7 @@ def seed_superadmin():
db.commit()
print(" Created superadmin permissions (existing user)")
# Create/update defaults template (special row with user_id = "__defaults__")
# Create/update defaults template
defaults = db.query(UserPermissions).filter(UserPermissions.user_id == "__defaults__").first()
if not defaults:
defaults = UserPermissions(user_id="__defaults__")
......@@ -56,6 +57,14 @@ def seed_superadmin():
db.commit()
print(" Created default permissions template")
# Seed app settings if missing
app_settings = db.query(AppSettings).first()
if not app_settings:
app_settings = AppSettings(registration_enabled=REGISTRATION_ENABLED_DEFAULT)
db.add(app_settings)
db.commit()
print(f" Created app settings (registration: {REGISTRATION_ENABLED_DEFAULT})")
except Exception as e:
print(f" Seed error: {e}")
finally:
......
This diff is collapsed.
This diff is collapsed.
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useApp } from "../store";
import { login, register } from "../api";
import { Flame, Eye, EyeOff, Loader2 } from "lucide-react";
import { login, register, getRegistrationStatus } from "../api";
import { Flame, LogIn, UserPlus, Eye, EyeOff, AlertCircle } from "lucide-react";
export default function LoginPage() {
const { dispatch } = useApp();
const [isRegister, setIsRegister] = useState(false);
const [isLogin, setIsLogin] = useState(true);
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPw, setShowPw] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [registrationEnabled, setRegistrationEnabled] = useState(true);
const [checkingRegistration, setCheckingRegistration] = useState(true);
useEffect(() => {
(async () => {
try {
const data = await getRegistrationStatus();
setRegistrationEnabled(data.registration_enabled);
} catch {
setRegistrationEnabled(true);
} finally {
setCheckingRegistration(false);
}
})();
}, []);
async function handleSubmit(e) {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = isRegister
? await register(username, email, password)
: await login(username, password);
dispatch({ type: "LOGIN", token: res.token, user: res.user });
let data;
if (isLogin) {
data = await login(username, password);
} else {
data = await register(username, email, password);
}
dispatch({ type: "LOGIN", token: data.token, user: data.user });
} catch (err) {
setError(err.message || "Authentication failed");
setError(err.message || "Something went wrong");
} finally {
setLoading(false);
}
}
return (
<div className="h-full h-dvh flex items-center justify-center bg-anton-bg px-4 safe-top safe-bottom">
<div className="w-full max-w-sm">
{/* Logo */}
<div className="min-h-screen bg-anton-bg flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<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" />
</div>
<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>
<h1 className="text-3xl font-bold text-white">Son of Anton</h1>
<p className="text-anton-muted mt-1 text-sm">Avatar of All Elements of Code</p>
</div>
<div className="bg-anton-card border border-anton-border rounded-2xl p-6 shadow-xl">
{!checkingRegistration && registrationEnabled && (
<div className="flex mb-6 bg-anton-bg rounded-xl p-1">
<button onClick={() => { setIsLogin(true); setError(""); }}
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition ${isLogin ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"}`}>
Sign In
</button>
<button onClick={() => { setIsLogin(false); setError(""); }}
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition ${!isLogin ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"}`}>
Register
</button>
</div>
)}
{error && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-2 text-red-400 text-sm">
<AlertCircle size={16} className="shrink-0" />
{error}
</div>
)}
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="text-xs text-anton-muted mb-1.5 block">Username</label>
<input
type="text"
value={username}
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"
placeholder="Enter username"
required
autoComplete="username"
autoCapitalize="off"
/>
<label className="text-xs text-anton-muted mb-1 block">Username</label>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)}
placeholder="Enter username" required autoFocus
className="w-full bg-anton-bg border border-anton-border rounded-xl px-4 py-3 text-white text-sm focus:outline-none focus:border-anton-accent transition" />
</div>
{isRegister && (
{!isLogin && (
<div>
<label className="text-xs text-anton-muted mb-1.5 block">Email</label>
<input
type="email"
value={email}
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"
placeholder="your@email.com"
required
autoComplete="email"
/>
<label className="text-xs text-anton-muted mb-1 block">Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email" required
className="w-full bg-anton-bg border border-anton-border rounded-xl px-4 py-3 text-white text-sm focus:outline-none focus:border-anton-accent transition" />
</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
type={showPw ? "text" : "password"}
value={password}
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"
placeholder="••••••••"
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} />}
<input type={showPassword ? "text" : "password"} value={password} onChange={(e) => setPassword(e.target.value)}
placeholder="Enter password" required
className="w-full bg-anton-bg border border-anton-border rounded-xl px-4 py-3 pr-10 text-white text-sm focus:outline-none focus:border-anton-accent transition" />
<button type="button" onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition">
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</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 bg-anton-accent text-white rounded-xl py-3 font-medium hover:opacity-90 transition disabled:opacity-50 flex items-center justify-center gap-2">
{loading ? (
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : isLogin ? (
<><LogIn size={18} /> Sign In</>
) : (
<><UserPlus size={18} /> Create Account</>
)}
<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
type="button"
onClick={() => { setIsRegister(!isRegister); setError(""); }}
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"}
</button>
</form>
{!checkingRegistration && !registrationEnabled && (
<div className="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg text-yellow-400 text-xs text-center">
Registration is currently disabled. Contact your administrator.
</div>
)}
</div>
<p className="text-center text-anton-muted text-xs mt-6">
Created by Mahmoud Aglan — AL-Arcade
</p>
</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