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:
......
......@@ -5,117 +5,252 @@ function headers(token) {
if (token) h["Authorization"] = `Bearer ${token}`;
return h;
}
function authHeader(token) { return token ? { Authorization: `Bearer ${token}` } : {}; }
function extractError(err, d) { let m = err.detail || err.message || d; if (Array.isArray(m)) return m.map(x => x.msg || JSON.stringify(x)).join(", "); if (typeof m === "object") return m.message || JSON.stringify(m); return String(m); }
function authHeader(token) {
return token ? { Authorization: `Bearer ${token}` } : {};
}
async function request(method, path, token, body) {
const opts = { method, headers: headers(token) };
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(extractError(err, "Request failed")); }
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(err.detail || err.message || "Request failed");
}
return res.json();
}
// Auth
export const login = (u, p) => request("POST", "/auth/login", null, { username: u, password: p });
export const register = (u, e, p) => request("POST", "/auth/register", null, { username: u, email: e, password: p });
export const getMe = (t) => request("GET", "/auth/me", t);
// Chats
export const listChats = (t) => request("GET", "/chats", t);
export const createChat = (t, d = {}) => request("POST", "/chats", t, d);
export const updateChat = (t, id, d) => request("PUT", `/chats/${id}`, t, d);
export const renameChat = (t, id, title) => updateChat(t, id, { title });
export const deleteChat = (t, id) => request("DELETE", `/chats/${id}`, t);
export const getMessages = (t, id) => request("GET", `/chats/${id}/messages`, t);
export const checkGenerating = (t, id) => request("GET", `/chats/${id}/generating`, t);
export const refreshRepoContext = (t, id) => request("POST", `/chats/${id}/refresh-repo`, t);
export const commitFromChat = (t, id, d) => request("POST", `/chats/${id}/commit`, t, d);
// Streaming
// ═══════════════════════════════════════
// Auth
// ═══════════════════════════════════════
export const getRegistrationStatus = () => request("GET", "/auth/registration-status", null);
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);
// ═══════════════════════════════════════
// Chats
// ═══════════════════════════════════════
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(extractError(err, "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 { } }
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 const checkGenerating = (token, chatId) =>
request("GET", `/chats/${chatId}/generating`, token);
export async function commitFromChat(token, chatId, data) {
return request("POST", `/chats/${chatId}/commit`, token, data);
}
export const refreshRepoContext = (token, chatId) =>
request("POST", `/chats/${chatId}/refresh-repo`, token);
// ═══════════════════════════════════════
// Attachments
// ═══════════════════════════════════════
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);
// ═══════════════════════════════════════
// Knowledge Base
// ═══════════════════════════════════════
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 const deleteKnowledgeDocument = (token, kbId, docId) =>
request("DELETE", `/knowledge/${kbId}/documents/${docId}`, 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]);
// ═══════════════════════════════════════
// Admin
// ═══════════════════════════════════════
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 adminGetAppSettings = (token) => request("GET", "/admin/app-settings", token);
export const adminUpdateAppSettings = (token, data) => request("PUT", "/admin/app-settings", token, data);
export const adminGetPermissionDefaults = (token) => request("GET", "/admin/permissions/defaults", token);
export const adminUpdatePermissionDefaults = (token, data) => request("PUT", "/admin/permissions/defaults", token, data);
export const adminApplyDefaults = (token) => request("POST", "/admin/permissions/apply-defaults", token);
export const adminGetUserPermissions = (token, userId) => request("GET", `/admin/users/${userId}/permissions`, token);
export const adminUpdateUserPermissions = (token, userId, data) => request("PUT", `/admin/users/${userId}/permissions`, token, data);
export const adminListModels = (token) => request("GET", "/admin/models", token);
// ═══════════════════════════════════════
// Files / Export
// ═══════════════════════════════════════
export async function downloadZip(token, markdown, title) {
const res = await fetch(`${BASE}/files/download-zip`, {
method: "POST", headers: headers(token),
body: JSON.stringify({ markdown, title }),
});
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);
}
}
// Attachments
export async function uploadAttachments(t, chatId, files) { const form = new FormData(); for (const f of files) form.append("files", f); const res = await fetch(`${BASE}/chats/${chatId}/attachments`, { method: "POST", headers: authHeader(t), body: form }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "Upload failed")); } return res.json(); }
export function getAttachmentUrl(id) { return `${BASE}/attachments/${id}/file`; }
export const deleteAttachment = (t, id) => request("DELETE", `/attachments/${id}`, t);
// Knowledge
export const listKnowledgeBases = (t) => request("GET", "/knowledge", t);
export const createKnowledgeBase = (t, n, d = "") => request("POST", "/knowledge", t, { name: n, description: d });
export const getKnowledgeBase = (t, id) => request("GET", `/knowledge/${id}`, t);
export const updateKnowledgeBase = (t, id, d) => request("PUT", `/knowledge/${id}`, t, d);
export const deleteKnowledgeBase = (t, id) => request("DELETE", `/knowledge/${id}`, t);
export const listKnowledgeDocuments = (t, id) => request("GET", `/knowledge/${id}/documents`, t);
export const deleteKnowledgeDocument = (t, kbId, docId) => request("DELETE", `/knowledge/${kbId}/documents/${docId}`, t);
export async function uploadDocuments(t, kbId, files) { const form = new FormData(); for (const f of files) form.append("files", f); const res = await fetch(`${BASE}/knowledge/${kbId}/upload`, { method: "POST", headers: authHeader(t), body: form }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "Upload failed")); } return res.json(); }
export const uploadDocument = (t, kbId, f) => uploadDocuments(t, kbId, [f]);
// Admin
export const adminStats = (t) => request("GET", "/admin/stats", t);
export const adminListUsers = (t) => request("GET", "/admin/users", t);
export const adminCreateUser = (t, d) => request("POST", "/admin/users", t, d);
export const adminUpdateUser = (t, id, d) => request("PUT", `/admin/users/${id}`, t, d);
export const adminDeleteUser = (t, id) => request("DELETE", `/admin/users/${id}`, t);
export const adminListChats = (t) => request("GET", "/admin/chats", t);
// Admin — Permissions
export const adminGetUserPermissions = (t, uid) => request("GET", `/admin/users/${uid}/permissions`, t);
export const adminUpdateUserPermissions = (t, uid, d) => request("PUT", `/admin/users/${uid}/permissions`, t, d);
export const adminGetDefaultPermissions = (t) => request("GET", "/admin/permissions/defaults", t);
export const adminUpdateDefaultPermissions = (t, d) => request("PUT", "/admin/permissions/defaults", t, d);
export const adminApplyDefaults = (t) => request("POST", "/admin/permissions/apply-defaults", t);
export const adminGetModels = (t) => request("GET", "/admin/models", t);
// Code Download
export async function downloadZip(t, md, title) { const res = await fetch(`${BASE}/files/download-zip`, { method: "POST", headers: headers(t), body: JSON.stringify({ markdown: md, title: title || null }) }); 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; const raw = (title || "").trim(); a.download = `${raw && raw !== "New Chat" ? raw.replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 60) || "code" : "code"}.zip`; a.click(); URL.revokeObjectURL(url); } else { const data = await res.json(); if (data.error) throw new Error(data.error); } }
// Export PPTX / DOCX
export async function exportPptx(token, markdown, title) {
const res = await fetch(`${BASE}/export/pptx`, { method: "POST", headers: headers(token), body: JSON.stringify({ markdown, title }) });
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "PPTX export failed")); }
const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url;
const safe = (title || "presentation").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 50) || "presentation";
a.download = `${safe}.pptx`; a.click(); URL.revokeObjectURL(url);
const res = await fetch(`${BASE}/export/pptx`, {
method: "POST", headers: headers(token),
body: JSON.stringify({ markdown, title }),
});
if (!res.ok) throw new Error("Export failed");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = (title || "presentation") + ".pptx";
a.click();
URL.revokeObjectURL(url);
}
export async function exportDocx(token, markdown, title) {
const res = await fetch(`${BASE}/export/docx`, { method: "POST", headers: headers(token), body: JSON.stringify({ markdown, title }) });
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "DOCX export failed")); }
const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url;
const safe = (title || "document").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 50) || "document";
a.download = `${safe}.docx`; a.click(); URL.revokeObjectURL(url);
const res = await fetch(`${BASE}/export/docx`, {
method: "POST", headers: headers(token),
body: JSON.stringify({ markdown, title }),
});
if (!res.ok) throw new Error("Export failed");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = (title || "document") + ".docx";
a.click();
URL.revokeObjectURL(url);
}
// Utilities
const CODE_BLOCK_RE = /```(\S*?)(?::(\S+?))?\s*?\n([\s\S]*?)```/g;
export function extractCodeBlocks(md) { if (!md) return []; const blocks = []; let m; const re = new RegExp(CODE_BLOCK_RE.source, "g"); while ((m = re.exec(md)) !== null) { const lang = (m[1] || "text").toLowerCase(); const fn = m[2] || null; const code = (m[3] || "").trim(); if (code) blocks.push({ language: lang, filename: fn, code }); } return blocks; }
// GitLab
export const gitlabGetSettings = (t) => request("GET", "/gitlab/settings", t);
export const gitlabUpdateSettings = (t, d) => request("PUT", "/gitlab/settings", t, d);
export const gitlabTestConnection = (t) => request("POST", "/gitlab/test-connection", t);
export const gitlabSearchProjects = (t, s, o) => request("GET", `/gitlab/projects?search=${encodeURIComponent(s || "")}&owned=${o || false}`, t);
export const gitlabCreateProject = (t, d) => request("POST", "/gitlab/projects", t, d);
export const gitlabListRepos = (t) => request("GET", "/gitlab/repos", t);
export const gitlabLinkRepo = (t, pid) => request("POST", "/gitlab/repos", t, { gitlab_project_id: pid });
export const gitlabUnlinkRepo = (t, id) => request("DELETE", `/gitlab/repos/${id}`, t);
export const gitlabGetTree = (t, id, p, r) => request("GET", `/gitlab/repos/${id}/tree?path=${encodeURIComponent(p || "")}&ref=${encodeURIComponent(r || "")}`, t);
export const gitlabGetFile = (t, id, p, r) => request("GET", `/gitlab/repos/${id}/file?path=${encodeURIComponent(p)}&ref=${encodeURIComponent(r || "")}`, t);
export const gitlabGetBranches = (t, id) => request("GET", `/gitlab/repos/${id}/branches`, t);
export const gitlabCreateBranch = (t, id, d) => request("POST", `/gitlab/repos/${id}/branches`, t, d);
export const gitlabCommit = (t, id, d) => request("POST", `/gitlab/repos/${id}/commit`, t, d);
export const gitlabCommitSingle = (t, id, d) => request("POST", `/gitlab/repos/${id}/commit-single`, t, d);
export const gitlabCreateMR = (t, id, d) => request("POST", `/gitlab/repos/${id}/merge-request`, t, d);
export const gitlabReanalyzeRepo = (t, id) => request("POST", `/gitlab/repos/${id}/analyze`, t);
export const gitlabGetRepoMap = (t, id) => request("GET", `/gitlab/repos/${id}/map`, t);
export const gitlabListActions = (t, s) => request("GET", `/gitlab/actions?status=${s || "pending"}`, t);
export const gitlabCreateAction = (t, d) => request("POST", "/gitlab/actions", t, d);
export const gitlabApproveAction = (t, id) => request("POST", `/gitlab/actions/${id}/approve`, t);
export const gitlabRejectAction = (t, id) => request("POST", `/gitlab/actions/${id}/reject`, t);
\ No newline at end of file
// ═══════════════════════════════════════
// GitLab
// ═══════════════════════════════════════
export const gitlabGetSettings = (token) => request("GET", "/gitlab/settings", token);
export const gitlabUpdateSettings = (token, data) => request("PUT", "/gitlab/settings", token, data);
export const gitlabTestConnection = (token) => request("POST", "/gitlab/test-connection", token);
export const gitlabSearchProjects = (token, search, owned) => request("GET", `/gitlab/projects?search=${encodeURIComponent(search || "")}&owned=${owned || false}`, token);
export const gitlabCreateProject = (token, data) => request("POST", "/gitlab/projects", token, data);
export const gitlabListRepos = (token) => request("GET", "/gitlab/repos", token);
export const gitlabLinkRepo = (token, projectId) => request("POST", "/gitlab/repos", token, { gitlab_project_id: projectId });
export const gitlabUnlinkRepo = (token, repoId) => request("DELETE", `/gitlab/repos/${repoId}`, token);
export const gitlabGetTree = (token, repoId, path, ref) => request("GET", `/gitlab/repos/${repoId}/tree?path=${encodeURIComponent(path || "")}&ref=${encodeURIComponent(ref || "")}`, token);
export const gitlabGetFile = (token, repoId, path, ref) => request("GET", `/gitlab/repos/${repoId}/file?path=${encodeURIComponent(path)}&ref=${encodeURIComponent(ref || "")}`, token);
export const gitlabGetBranches = (token, repoId) => request("GET", `/gitlab/repos/${repoId}/branches`, token);
export const gitlabCommit = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/commit`, token, data);
export const gitlabCommitSingle = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/commit-single`, token, data);
export const gitlabCreateBranch = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/branches`, token, data);
export const gitlabCreateMR = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/merge-request`, token, data);
export const gitlabAnalyzeRepo = (token, repoId) => request("POST", `/gitlab/repos/${repoId}/analyze`, token);
export const gitlabGetRepoMap = (token, repoId) => request("GET", `/gitlab/repos/${repoId}/map`, token);
export const gitlabListActions = (token, status) => request("GET", `/gitlab/actions?status=${status || "pending"}`, token);
export const gitlabCreateAction = (token, data) => request("POST", "/gitlab/actions", token, data);
export const gitlabApproveAction = (token, actionId) => request("POST", `/gitlab/actions/${actionId}/approve`, token);
export const gitlabRejectAction = (token, actionId) => request("POST", `/gitlab/actions/${actionId}/reject`, token);
\ No newline at end of file
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useApp } from "../store";
import {
adminStats, adminListUsers, adminCreateUser, adminUpdateUser, adminDeleteUser,
adminListChats, adminGetAppSettings, adminUpdateAppSettings,
adminGetPermissionDefaults, adminUpdatePermissionDefaults, adminApplyDefaults,
adminGetUserPermissions, adminUpdateUserPermissions,
adminGetDefaultPermissions, adminUpdateDefaultPermissions, adminApplyDefaults,
} from "../api";
import {
ArrowLeft, Users, MessageSquare, Database, Zap,
UserPlus, Trash2, Shield, ShieldOff, Save, X,
Settings2, Check, Globe, Paintbrush, BookOpen, GitBranch,
Paperclip, Presentation, FileOutput, Cpu, Gauge, Lock,
Loader2, RotateCcw, Copy,
Users, MessageSquare, Brain, Database, Shield, Plus, Trash2, Edit, Check, X,
BarChart3, Settings2, ChevronDown, ChevronRight, ToggleLeft, ToggleRight, UserPlus,
RefreshCw, Lock, Unlock, Save,
} from "lucide-react";
const MODELS = [
{ id: "eu.anthropic.claude-opus-4-6-v1", label: "Opus 4.6", tier: "💰 Expensive" },
{ id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", label: "Haiku 4.5", tier: "⚡ Cheap" },
];
const FEATURE_DEFS = [
{ key: "can_use_web_search", label: "Web Search", icon: Globe, color: "text-green-400", desc: "SerpAPI calls — costs money per search" },
{ key: "can_use_ui_design", label: "UI Design Mode", icon: Paintbrush, color: "text-blue-400", desc: "Generates full HTML — uses more tokens" },
{ key: "can_use_knowledge_base", label: "Knowledge Bases", icon: BookOpen, color: "text-emerald-400", desc: "RAG document upload & query" },
{ key: "can_use_gitlab", label: "GitLab Access", icon: GitBranch, color: "text-orange-400", desc: "Repository linking & commits" },
{ key: "can_use_attachments", label: "File Attachments", icon: Paperclip, color: "text-cyan-400", desc: "Upload images, videos, documents" },
{ key: "can_export_pptx", label: "Export PPTX", icon: Presentation, color: "text-amber-400", desc: "Download as PowerPoint" },
{ key: "can_export_docx", label: "Export DOCX", icon: FileOutput, color: "text-indigo-400", desc: "Download as Word document" },
];
const LIMIT_DEFS = [
{ key: "max_tokens_cap", label: "Max Output Tokens", min: 256, max: 65536, step: 256, desc: "Maximum output tokens user can set" },
{ key: "max_reasoning_budget", label: "Max Reasoning Budget", min: 0, max: 32000, step: 500, desc: "0 = reasoning disabled" },
{ key: "max_chats", label: "Max Chats", min: 0, max: 1000, step: 1, desc: "0 = unlimited" },
{ key: "max_messages_per_day", label: "Messages / Day", min: 0, max: 10000, step: 10, desc: "0 = unlimited" },
{ key: "max_knowledge_bases", label: "Max Knowledge Bases", min: 0, max: 100, step: 1, desc: "0 = unlimited" },
{ key: "max_documents_per_kb", label: "Max Docs per KB", min: 0, max: 500, step: 5, desc: "0 = unlimited" },
{ key: "max_attachment_size_mb", label: "Attachment Size (MB)", min: 1, max: 100, step: 1, desc: "Per-file upload limit" },
{ key: "max_attachments_per_message", label: "Attachments / Message", min: 1, max: 50, step: 1, desc: "Files per single message" },
];
function Toggle({ value, onChange }) {
return (
<button onClick={() => onChange(!value)} className={`w-10 h-5 rounded-full transition-colors relative ${value ? "bg-green-500" : "bg-anton-border"}`}>
<div className={`w-4 h-4 rounded-full bg-white shadow absolute top-0.5 transition-transform ${value ? "translate-x-5.5 left-0.5" : "left-0.5"}`} style={{ transform: value ? "translateX(20px)" : "translateX(0)" }} />
</button>
);
}
export default function AdminPage() {
const { state } = useApp();
const navigate = useNavigate();
const [tab, setTab] = useState("stats");
const [stats, setStats] = useState(null);
const [users, setUsers] = useState([]);
const [chats, setChats] = useState([]);
const [appSettings, setAppSettings] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
// Create user form
const [showCreate, setShowCreate] = useState(false);
const [editId, setEditId] = useState(null);
const [editData, setEditData] = useState({});
const [newUser, setNewUser] = useState({ username: "", email: "", password: "", role: "user", quota_tokens_monthly: 2000000 });
const [error, setError] = useState("");
// Permissions modal
const [permsUser, setPermsUser] = useState(null); // user object or { id: "__defaults__", username: "Default Template" }
const [permsData, setPermsData] = useState(null);
const [permsSaving, setPermsSaving] = useState(false);
const [permsLoading, setPermsLoading] = useState(false);
const [applyingDefaults, setApplyingDefaults] = useState(false);
// Edit user
const [editUserId, setEditUserId] = useState(null);
const [editData, setEditData] = useState({});
// Permissions
const [permUserId, setPermUserId] = useState(null);
const [permData, setPermData] = useState(null);
const [defaults, setDefaults] = useState(null);
const token = state.token;
const load = useCallback(async () => {
setLoading(true);
setError("");
try {
const [s, u] = await Promise.all([adminStats(state.token), adminListUsers(state.token)]);
setStats(s); setUsers(u);
} catch (err) { setError(err.message); }
}, [state.token]);
const [s, u, c, as_] = await Promise.all([
adminStats(token), adminListUsers(token), adminListChats(token), adminGetAppSettings(token),
]);
setStats(s); setUsers(u); setChats(c); setAppSettings(as_);
} catch (e) { setError(e.message); } finally { setLoading(false); }
}, [token]);
useEffect(() => { load(); }, [load]);
async function handleCreate(e) {
e.preventDefault();
try { await adminCreateUser(state.token, newUser); setShowCreate(false); setNewUser({ username: "", email: "", password: "", role: "user", quota_tokens_monthly: 2000000 }); load(); } catch (err) { setError(err.message); }
}
async function handleSaveEdit(userId) {
try { await adminUpdateUser(state.token, userId, editData); setEditId(null); load(); } catch (err) { setError(err.message); }
}
function flash(msg) { setSuccess(msg); setTimeout(() => setSuccess(""), 3000); }
async function handleDelete(userId, username) {
if (!confirm(`Delete user "${username}"? This is permanent.`)) return;
try { await adminDeleteUser(state.token, userId); load(); } catch (err) { setError(err.message); }
async function toggleRegistration() {
try {
const res = await adminUpdateAppSettings(token, { registration_enabled: !appSettings.registration_enabled });
setAppSettings(res);
flash(res.registration_enabled ? "Registration ENABLED" : "Registration DISABLED");
} catch (e) { setError(e.message); }
}
// ═══ Permissions ═══
async function openPerms(user) {
setPermsUser(user); setPermsLoading(true); setPermsData(null);
async function handleCreateUser(e) {
e.preventDefault();
try {
const data = user.id === "__defaults__"
? await adminGetDefaultPermissions(state.token)
: await adminGetUserPermissions(state.token, user.id);
setPermsData(data);
} catch (err) { setError(err.message); setPermsUser(null); }
setPermsLoading(false);
await adminCreateUser(token, newUser);
setShowCreate(false); setNewUser({ username: "", email: "", password: "", role: "user", quota_tokens_monthly: 2000000 });
flash("User created"); load();
} catch (e) { setError(e.message); }
}
async function savePerms() {
if (!permsUser || !permsData) return;
setPermsSaving(true);
async function handleUpdateUser(userId) {
try {
if (permsUser.id === "__defaults__") {
await adminUpdateDefaultPermissions(state.token, permsData);
} else {
await adminUpdateUserPermissions(state.token, permsUser.id, permsData);
}
setPermsUser(null); setPermsData(null);
} catch (err) { setError(err.message); }
setPermsSaving(false);
await adminUpdateUser(token, userId, editData);
setEditUserId(null); flash("User updated"); load();
} catch (e) { setError(e.message); }
}
async function handleApplyDefaults() {
if (!confirm("Apply default permissions to ALL regular users? This will overwrite their current permissions.")) return;
setApplyingDefaults(true);
try {
const res = await adminApplyDefaults(state.token);
alert(`✅ Applied to ${res.users_updated} users`);
} catch (err) { setError(err.message); }
setApplyingDefaults(false);
async function handleDeleteUser(userId, username) {
if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return;
try { await adminDeleteUser(token, userId); flash("User deleted"); load(); } catch (e) { setError(e.message); }
}
function updatePerm(key, value) {
setPermsData(prev => ({ ...prev, [key]: value }));
async function handleToggleActive(userId, currentActive) {
try { await adminUpdateUser(token, userId, { is_active: !currentActive }); flash("User updated"); load(); } catch (e) { setError(e.message); }
}
function formatNum(n) {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(0) + "K";
return String(n);
async function openPermissions(userId) {
try {
const [p, d] = await Promise.all([adminGetUserPermissions(token, userId), adminGetPermissionDefaults(token)]);
setPermUserId(userId); setPermData(p); setDefaults(d);
} catch (e) { setError(e.message); }
}
if (state.user?.role !== "superadmin") {
return <div className="h-full flex items-center justify-center"><p className="text-anton-danger text-lg">⛔ Access Denied</p></div>;
async function savePermissions() {
try { await adminUpdateUserPermissions(token, permUserId, permData); setPermUserId(null); flash("Permissions saved"); } catch (e) { setError(e.message); }
}
const TABS = [
{ id: "stats", label: "Dashboard", icon: BarChart3 },
{ id: "users", label: "Users", icon: Users },
{ id: "settings", label: "Settings", icon: Settings2 },
];
return (
<div className="h-full overflow-y-auto bg-anton-bg p-4 sm:p-6">
<div className="max-w-6xl mx-auto space-y-6 animate-fade-in">
{/* Header */}
<div className="flex items-center gap-4">
<button onClick={() => navigate("/")} className="p-2 rounded-lg bg-anton-surface border border-anton-border hover:border-anton-accent transition"><ArrowLeft size={20} /></button>
<div className="flex-1 flex flex-col min-h-0 bg-anton-bg">
{/* Header */}
<div className="border-b border-anton-border bg-anton-surface px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white flex items-center gap-2"><Shield size={24} className="text-anton-accent" /> Admin Panel</h1>
<p className="text-anton-muted text-sm">Users, permissions & cost control</p>
<h1 className="text-xl font-bold text-white flex items-center gap-2"><Shield size={20} className="text-anton-accent" /> Admin Dashboard</h1>
<p className="text-xs text-anton-muted mt-0.5">Superadmin Control Panel</p>
</div>
<button onClick={load} className="text-anton-muted hover:text-white transition p-2 rounded-lg hover:bg-anton-card"><RefreshCw size={16} /></button>
</div>
<div className="flex gap-1 mt-4">
{TABS.map((t) => (
<button key={t.id} onClick={() => setTab(t.id)}
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition ${tab === t.id ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white hover:bg-anton-card"}`}>
<t.icon size={14} /> {t.label}
</button>
))}
</div>
</div>
{/* Notifications */}
{error && <div className="mx-6 mt-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center justify-between">{error} <button onClick={() => setError("")}><X size={14} /></button></div>}
{success && <div className="mx-6 mt-3 p-3 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400 text-sm">{success}</div>}
{error && (<div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3">{error}<button onClick={() => setError("")} className="ml-2 text-red-300 hover:text-white"></button></div>)}
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{loading && !stats && <div className="text-center text-anton-muted py-12">Loading...</div>}
{/* Stats */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* ═══ STATS TAB ═══ */}
{tab === "stats" && stats && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{[
{ label: "Users", value: stats.total_users, icon: Users, color: "text-blue-400" },
{ label: "Chats", value: stats.total_chats, icon: MessageSquare, color: "text-green-400" },
{ label: "Messages", value: formatNum(stats.total_messages), icon: Zap, color: "text-anton-accent" },
{ label: "Tokens Used", value: formatNum(stats.total_tokens_used), icon: Database, color: "text-purple-400" },
].map((s) => (
<div key={s.label} className="bg-anton-surface border border-anton-border rounded-xl p-4">
<div className="flex items-center gap-2 mb-1"><s.icon size={16} className={s.color} /><span className="text-anton-muted text-sm">{s.label}</span></div>
<p className="text-2xl font-bold text-white">{s.value}</p>
{ label: "Active", value: stats.active_users, icon: Check, color: "text-green-400" },
{ label: "Chats", value: stats.total_chats, icon: MessageSquare, color: "text-purple-400" },
{ label: "Messages", value: stats.total_messages, icon: MessageSquare, color: "text-cyan-400" },
{ label: "Tokens Used", value: (stats.total_tokens_used / 1000000).toFixed(1) + "M", icon: Brain, color: "text-yellow-400" },
{ label: "Knowledge", value: stats.total_knowledge_bases, icon: Database, color: "text-orange-400" },
].map((s, i) => (
<div key={i} className="bg-anton-card border border-anton-border rounded-xl p-4">
<div className="flex items-center gap-2 mb-2"><s.icon size={14} className={s.color} /><span className="text-xs text-anton-muted">{s.label}</span></div>
<div className="text-2xl font-bold text-white">{typeof s.value === "number" ? s.value.toLocaleString() : s.value}</div>
</div>
))}
</div>
)}
{/* User Management */}
<div className="bg-anton-surface border border-anton-border rounded-xl overflow-hidden">
<div className="px-5 py-4 border-b border-anton-border flex items-center justify-between flex-wrap gap-2">
<h2 className="text-lg font-semibold text-white">Users</h2>
<div className="flex items-center gap-2">
<button onClick={() => openPerms({ id: "__defaults__", username: "Default Template" })} className="flex items-center gap-1.5 px-3 py-1.5 bg-purple-500/20 text-purple-400 border border-purple-500/30 rounded-lg text-sm font-medium hover:bg-purple-500/30 transition" title="Edit default permissions for new users">
<Lock size={14} /> Defaults
</button>
<button onClick={handleApplyDefaults} disabled={applyingDefaults} className="flex items-center gap-1.5 px-3 py-1.5 bg-anton-card border border-anton-border text-anton-muted rounded-lg text-sm hover:text-white transition disabled:opacity-50">
{applyingDefaults ? <Loader2 size={14} className="animate-spin" /> : <Copy size={14} />} Apply to All
</button>
<button onClick={() => setShowCreate(!showCreate)} className="flex items-center gap-1.5 px-3 py-1.5 bg-anton-accent text-white rounded-lg text-sm font-medium hover:opacity-90 transition">
{showCreate ? <X size={14} /> : <UserPlus size={14} />} {showCreate ? "Cancel" : "New User"}
{/* ═══ USERS TAB ═══ */}
{tab === "users" && (
<>
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-white">Users ({users.length})</h2>
<button onClick={() => setShowCreate(!showCreate)} className="flex items-center gap-1.5 px-4 py-2 bg-anton-accent text-white rounded-lg text-sm hover:opacity-90 transition">
<Plus size={14} /> Create User
</button>
</div>
</div>
{showCreate && (
<form onSubmit={handleCreate} className="px-5 py-4 border-b border-anton-border bg-anton-card grid grid-cols-1 md:grid-cols-3 gap-3">
<input placeholder="Username" required value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent" />
<input placeholder="Email" type="email" required value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent" />
<input placeholder="Password" required value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent" />
<select value={newUser.role} onChange={(e) => setNewUser({ ...newUser, role: e.target.value })} className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent">
<option value="user">User</option><option value="admin">Admin</option><option value="superadmin">Superadmin</option>
</select>
<input placeholder="Monthly quota" type="number" value={newUser.quota_tokens_monthly} onChange={(e) => setNewUser({ ...newUser, quota_tokens_monthly: Number(e.target.value) })} className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent" />
<button type="submit" className="bg-anton-success text-white rounded-lg px-3 py-2 text-sm font-medium hover:opacity-90">Create</button>
</form>
)}
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-anton-muted border-b border-anton-border">
<th className="px-5 py-3">User</th><th className="px-5 py-3">Role</th><th className="px-5 py-3">Quota</th><th className="px-5 py-3">Used</th><th className="px-5 py-3">Chats</th><th className="px-5 py-3">Status</th><th className="px-5 py-3">Actions</th>
</tr></thead>
<tbody>
{users.map((u) => (
<tr key={u.id} className="border-b border-anton-border/50 hover:bg-anton-card/50 transition">
<td className="px-5 py-3"><div className="text-white font-medium">{u.username}</div><div className="text-anton-muted text-xs">{u.email}</div></td>
<td className="px-5 py-3">
{editId === u.id ? (
<select value={editData.role ?? u.role} onChange={(e) => setEditData({ ...editData, role: e.target.value })} className="bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs">
<option value="user">user</option><option value="admin">admin</option><option value="superadmin">superadmin</option>
</select>
) : (
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${u.role === "superadmin" ? "bg-anton-accent/20 text-anton-accent" : u.role === "admin" ? "bg-blue-500/20 text-blue-400" : "bg-anton-border text-anton-muted"}`}>{u.role}</span>
)}
</td>
<td className="px-5 py-3 text-anton-muted">
{editId === u.id ? <input type="number" value={editData.quota_tokens_monthly ?? u.quota_tokens_monthly} onChange={(e) => setEditData({ ...editData, quota_tokens_monthly: Number(e.target.value) })} className="bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs w-28" /> : formatNum(u.quota_tokens_monthly)}
</td>
<td className="px-5 py-3 text-anton-muted">{formatNum(u.tokens_used_this_month)}</td>
<td className="px-5 py-3 text-anton-muted">{u.chat_count}</td>
<td className="px-5 py-3"><span className={`w-2 h-2 inline-block rounded-full mr-1 ${u.is_active ? "bg-anton-success" : "bg-anton-danger"}`} /><span className="text-xs text-anton-muted">{u.is_active ? "Active" : "Off"}</span></td>
<td className="px-5 py-3">
<div className="flex items-center gap-1">
{editId === u.id ? (
<><button onClick={() => handleSaveEdit(u.id)} className="p-1 rounded hover:bg-anton-success/20 text-anton-success"><Save size={14} /></button><button onClick={() => setEditId(null)} className="p-1 rounded hover:bg-anton-border text-anton-muted"><X size={14} /></button></>
) : (
<>
{u.role !== "superadmin" && (
<button onClick={() => openPerms(u)} className="p-1 rounded hover:bg-purple-500/20 text-purple-400" title="Permissions"><Settings2 size={14} /></button>
)}
<button onClick={() => { setEditId(u.id); setEditData({}); }} className="p-1 rounded hover:bg-anton-accent/20 text-anton-accent text-xs">Edit</button>
<button onClick={() => adminUpdateUser(state.token, u.id, { is_active: !u.is_active }).then(load)} className="p-1 rounded hover:bg-anton-border text-anton-muted" title={u.is_active ? "Disable" : "Enable"}>
{u.is_active ? <ShieldOff size={14} /> : <Shield size={14} />}
</button>
{u.role !== "superadmin" && <button onClick={() => handleDelete(u.id, u.username)} className="p-1 rounded hover:bg-red-500/20 text-anton-danger"><Trash2 size={14} /></button>}
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* ═══ PERMISSIONS MODAL ═══ */}
{permsUser && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-start justify-center overflow-y-auto p-4 animate-fade-in" onClick={() => { setPermsUser(null); setPermsData(null); }}>
<div className="bg-anton-surface border border-anton-border rounded-2xl w-full max-w-2xl my-8 shadow-2xl" onClick={e => e.stopPropagation()}>
{/* Modal Header */}
<div className="px-6 py-4 border-b border-anton-border flex items-center justify-between">
<div>
<h2 className="text-lg font-bold text-white flex items-center gap-2">
<Settings2 size={20} className="text-purple-400" />
{permsUser.id === "__defaults__" ? "Default Permissions Template" : `Permissions: @${permsUser.username}`}
</h2>
<p className="text-xs text-anton-muted mt-0.5">
{permsUser.id === "__defaults__" ? "Applied to new users on creation" : `Role: ${permsUser.role}`}
</p>
</div>
<button onClick={() => { setPermsUser(null); setPermsData(null); }} className="p-2 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"><X size={18} /></button>
</div>
{permsLoading ? (
<div className="flex items-center justify-center py-20"><Loader2 size={24} className="text-anton-accent animate-spin" /></div>
) : permsData ? (
<div className="p-6 space-y-6 max-h-[70vh] overflow-y-auto">
{/* Feature Access */}
<div>
<h3 className="text-sm font-semibold text-white mb-3 flex items-center gap-2"><Lock size={14} className="text-anton-accent" /> Feature Access</h3>
<div className="space-y-2">
{FEATURE_DEFS.map(f => {
const Icon = f.icon;
return (
<div key={f.key} className="flex items-center justify-between bg-anton-card rounded-lg px-4 py-3 border border-anton-border">
<div className="flex items-center gap-3">
<Icon size={16} className={f.color} />
<div>
<p className="text-sm text-white font-medium">{f.label}</p>
<p className="text-[10px] text-anton-muted">{f.desc}</p>
</div>
</div>
<Toggle value={!!permsData[f.key]} onChange={v => updatePerm(f.key, v)} />
</div>
);
})}
</div>
{showCreate && (
<form onSubmit={handleCreateUser} className="bg-anton-card border border-anton-border rounded-xl p-4 space-y-3 animate-fade-in">
<div className="grid grid-cols-2 gap-3">
<input value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} placeholder="Username" required
className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent" />
<input value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} placeholder="Email" required type="email"
className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent" />
<input value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} placeholder="Password" required type="password"
className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent" />
<select value={newUser.role} onChange={(e) => setNewUser({ ...newUser, role: e.target.value })}
className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent">
<option value="user">User</option><option value="admin">Admin</option>
</select>
</div>
<div className="flex gap-2">
<button type="submit" className="px-4 py-2 bg-anton-accent text-white rounded-lg text-sm">Create</button>
<button type="button" onClick={() => setShowCreate(false)} className="px-4 py-2 bg-anton-card border border-anton-border text-white rounded-lg text-sm">Cancel</button>
</div>
</form>
)}
{/* Model Access */}
<div>
<h3 className="text-sm font-semibold text-white mb-3 flex items-center gap-2"><Cpu size={14} className="text-cyan-400" /> Model Access</h3>
<div className="space-y-2">
<label className="flex items-center gap-3 bg-anton-card rounded-lg px-4 py-3 border border-anton-border cursor-pointer">
<input type="checkbox" checked={permsData.allowed_models === "all"} onChange={e => updatePerm("allowed_models", e.target.checked ? "all" : MODELS.map(m => m.id).join(","))}
className="w-4 h-4 rounded accent-anton-accent" />
<div><p className="text-sm text-white font-medium">All Models</p><p className="text-[10px] text-anton-muted">Access to every available model</p></div>
</label>
{permsData.allowed_models !== "all" && (
<div className="pl-4 space-y-1.5">
{MODELS.map(m => {
const list = (permsData.allowed_models || "").split(",").map(s => s.trim()).filter(Boolean);
const checked = list.includes(m.id);
return (
<label key={m.id} className="flex items-center gap-3 bg-anton-bg rounded-lg px-3 py-2 border border-anton-border/50 cursor-pointer">
<input type="checkbox" checked={checked} onChange={e => {
let next = checked ? list.filter(x => x !== m.id) : [...list, m.id];
if (!next.length) next = [m.id]; // Can't have zero models
updatePerm("allowed_models", next.join(","));
}} className="w-4 h-4 rounded accent-anton-accent" />
<div><span className="text-sm text-white">{m.label}</span><span className="text-[10px] text-anton-muted ml-2">{m.tier}</span></div>
</label>
);
})}
</div>
)}
<div className="space-y-2">
{users.map((u) => (
<div key={u.id} className="bg-anton-card border border-anton-border rounded-xl p-4 flex items-center gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-white font-medium text-sm">{u.username}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded ${u.role === "superadmin" ? "bg-red-500/20 text-red-400" : u.role === "admin" ? "bg-yellow-500/20 text-yellow-400" : "bg-blue-500/20 text-blue-400"}`}>{u.role}</span>
{!u.is_active && <span className="text-[10px] px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">disabled</span>}
</div>
<div className="text-xs text-anton-muted mt-0.5">{u.email}{u.chat_count} chats • {((u.tokens_used_this_month || 0) / 1000000).toFixed(2)}M / {((u.quota_tokens_monthly || 0) / 1000000).toFixed(0)}M tokens</div>
</div>
{u.role !== "superadmin" && (
<div className="flex items-center gap-1">
<button onClick={() => handleToggleActive(u.id, u.is_active)} title={u.is_active ? "Disable" : "Enable"}
className="p-1.5 rounded-lg hover:bg-anton-bg transition text-anton-muted hover:text-white">
{u.is_active ? <Unlock size={14} /> : <Lock size={14} />}
</button>
<button onClick={() => openPermissions(u.id)} title="Permissions"
className="p-1.5 rounded-lg hover:bg-anton-bg transition text-anton-muted hover:text-purple-400">
<Shield size={14} />
</button>
<button onClick={() => handleDeleteUser(u.id, u.username)} title="Delete"
className="p-1.5 rounded-lg hover:bg-anton-bg transition text-anton-muted hover:text-red-400">
<Trash2 size={14} />
</button>
</div>
)}
</div>
))}
</div>
{/* Limits */}
<div>
<h3 className="text-sm font-semibold text-white mb-3 flex items-center gap-2"><Gauge size={14} className="text-amber-400" /> Usage Limits</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{LIMIT_DEFS.map(l => (
<div key={l.key} className="bg-anton-card rounded-lg px-4 py-3 border border-anton-border">
<div className="flex justify-between items-center mb-1">
<label className="text-xs text-anton-muted">{l.label}</label>
<span className="text-xs text-white font-mono">{(permsData[l.key] || 0) === 0 && l.desc?.includes("unlimited") ? "∞" : (permsData[l.key] || 0).toLocaleString()}</span>
</div>
<input type="number" min={l.min} max={l.max} step={l.step} value={permsData[l.key] || 0}
onChange={e => updatePerm(l.key, Math.min(Math.max(Number(e.target.value) || 0, l.min), l.max))}
className="w-full bg-anton-bg border border-anton-border rounded px-2.5 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-anton-accent"
/>
<p className="text-[9px] text-anton-muted mt-1">{l.desc}</p>
</div>
{/* Permissions Modal */}
{permUserId && permData && (
<div className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4" onClick={() => setPermUserId(null)}>
<div className="bg-anton-card border border-anton-border rounded-2xl p-6 w-full max-w-lg max-h-[80vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2"><Shield size={18} className="text-purple-400" /> User Permissions</h3>
<div className="space-y-3">
{["can_use_web_search", "can_use_ui_design", "can_use_knowledge_base", "can_use_gitlab", "can_use_attachments", "can_export_pptx", "can_export_docx"].map((key) => (
<label key={key} className="flex items-center justify-between">
<span className="text-sm text-white">{key.replace(/^can_/, "").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}</span>
<button onClick={() => setPermData({ ...permData, [key]: !permData[key] })}
className={`transition ${permData[key] ? "text-green-400" : "text-anton-muted"}`}>
{permData[key] ? <ToggleRight size={24} /> : <ToggleLeft size={24} />}
</button>
</label>
))}
<div className="border-t border-anton-border pt-3 space-y-3">
<div><label className="text-xs text-anton-muted">Allowed Models</label>
<input value={permData.allowed_models || ""} onChange={(e) => setPermData({ ...permData, allowed_models: e.target.value })}
className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm mt-1 focus:outline-none focus:border-anton-accent" /></div>
{["max_tokens_cap", "max_reasoning_budget", "max_chats", "max_messages_per_day", "max_knowledge_bases", "max_documents_per_kb", "max_attachment_size_mb", "max_attachments_per_message"].map((key) => (
<div key={key}><label className="text-xs text-anton-muted">{key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} {key.includes("max_") && permData[key] === 0 ? "(unlimited)" : ""}</label>
<input type="number" value={permData[key] || 0} onChange={(e) => setPermData({ ...permData, [key]: parseInt(e.target.value) || 0 })}
className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm mt-1 focus:outline-none focus:border-anton-accent" /></div>
))}
</div>
</div>
<div className="flex gap-2 mt-4">
<button onClick={savePermissions} className="flex items-center gap-1.5 px-4 py-2 bg-anton-accent text-white rounded-lg text-sm"><Save size={14} /> Save</button>
<button onClick={() => setPermUserId(null)} className="px-4 py-2 bg-anton-card border border-anton-border text-white rounded-lg text-sm">Cancel</button>
</div>
</div>
</div>
) : null}
)}
</>
)}
{/* Modal Footer */}
{permsData && (
<div className="px-6 py-4 border-t border-anton-border flex items-center justify-between">
<button onClick={() => { setPermsUser(null); setPermsData(null); }} className="px-4 py-2 rounded-lg border border-anton-border text-anton-muted hover:text-white transition text-sm">Cancel</button>
<button onClick={savePerms} disabled={permsSaving} className="flex items-center gap-2 px-5 py-2 bg-anton-accent text-white rounded-lg text-sm font-semibold hover:opacity-80 transition disabled:opacity-50">
{permsSaving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />} Save Permissions
</button>
{/* ═══ SETTINGS TAB ═══ */}
{tab === "settings" && (
<div className="space-y-6">
{/* Registration Toggle */}
<div className="bg-anton-card border border-anton-border rounded-xl p-6">
<h3 className="text-lg font-semibold text-white mb-1 flex items-center gap-2"><UserPlus size={18} className="text-blue-400" /> User Registration</h3>
<p className="text-xs text-anton-muted mb-4">Control whether new users can register accounts themselves.</p>
{appSettings && (
<div className="flex items-center justify-between bg-anton-bg border border-anton-border rounded-xl p-4">
<div>
<div className="text-sm text-white font-medium">
Registration is {appSettings.registration_enabled ? <span className="text-green-400">ENABLED</span> : <span className="text-red-400">DISABLED</span>}
</div>
<div className="text-xs text-anton-muted mt-0.5">
{appSettings.registration_enabled
? "Anyone can create a new account from the login page."
: "Only superadmins can create new user accounts."}
</div>
</div>
<button onClick={toggleRegistration}
className={`transition-colors ${appSettings.registration_enabled ? "text-green-400 hover:text-green-300" : "text-anton-muted hover:text-white"}`}>
{appSettings.registration_enabled ? <ToggleRight size={36} /> : <ToggleLeft size={36} />}
</button>
</div>
)}
</div>
{/* Recent Chats */}
<div className="bg-anton-card border border-anton-border rounded-xl p-6">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2"><MessageSquare size={18} className="text-purple-400" /> Recent Chats</h3>
<div className="space-y-2 max-h-80 overflow-y-auto">
{chats.slice(0, 50).map((c) => (
<div key={c.id} className="flex items-center justify-between bg-anton-bg border border-anton-border rounded-lg p-3">
<div className="min-w-0">
<div className="text-sm text-white truncate">{c.title}</div>
<div className="text-xs text-anton-muted">{c.username}{c.message_count} msgs</div>
</div>
<span className="text-[10px] text-anton-muted shrink-0">{new Date(c.updated_at).toLocaleDateString()}</span>
</div>
))}
{chats.length === 0 && <div className="text-center text-anton-muted text-sm py-4">No chats yet</div>}
</div>
)}
</div>
</div>
</div>
)}
)}
</div>
</div>
);
}
\ No newline at end of file
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>
{/* 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"
/>
</div>
{isRegister && (
<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"
/>
</div>
)}
<div>
<label className="text-xs text-anton-muted mb-1.5 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} />}
<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>
</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">
<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>
)}
<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>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<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>
{!isLogin && (
<div>
<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 block">Password</label>
<div className="relative">
<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>
<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>
</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>
<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>
<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