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") ...@@ -58,6 +58,11 @@ CHROMADB_PATH = os.getenv("CHROMADB_PATH", "/data/chromadb")
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
SERPAPI_KEY = os.getenv("SERPAPI_KEY", "") 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 # PERMISSION DEFAULTS — applied to new regular users
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
......
...@@ -48,12 +48,16 @@ def _run_migrations(): ...@@ -48,12 +48,16 @@ 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")
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"]: 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,14 @@ class UserPermissions(Base): ...@@ -81,6 +78,14 @@ class UserPermissions(Base):
user = relationship("User", back_populates="permissions") 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): class Chat(Base):
__tablename__ = "chats" __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 from pydantic import BaseModel
...@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session ...@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
from sqlalchemy import func from sqlalchemy import func
from backend.database import get_db 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 ( from backend.auth import (
require_superadmin, hash_password, get_user_permissions, require_superadmin, hash_password, get_user_permissions,
ensure_user_permissions, get_default_permissions_template, ensure_user_permissions, get_default_permissions_template,
...@@ -55,6 +55,10 @@ class PermissionsBody(BaseModel): ...@@ -55,6 +55,10 @@ class PermissionsBody(BaseModel):
max_attachments_per_message: Optional[int] = None max_attachments_per_message: Optional[int] = None
class AppSettingsBody(BaseModel):
registration_enabled: Optional[bool] = None
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
# Stats & Users # Stats & Users
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
...@@ -107,7 +111,6 @@ def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin), ...@@ -107,7 +111,6 @@ def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin),
db.add(user) db.add(user)
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)
return {"id": user.id, "username": user.username} return {"id": user.id, "username": user.username}
...@@ -160,6 +163,37 @@ def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depe ...@@ -160,6 +163,37 @@ def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depe
return result 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 # PERMISSIONS MANAGEMENT
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
......
""" """
Authentication routes: register, login, profile — with permissions. Authentication routes: register, login, profile — with permissions and 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("/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") @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
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( 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)
......
""" """
Seed superadmin user and default permissions template. Seed superadmin user, default permissions template, and app settings.
""" """
from backend.database import SessionLocal 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.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(): def seed_superadmin():
...@@ -25,7 +28,6 @@ def seed_superadmin(): ...@@ -25,7 +28,6 @@ def seed_superadmin():
db.refresh(user) db.refresh(user)
print(f" Created superadmin (password: {SUPERADMIN_PASSWORD})") print(f" Created superadmin (password: {SUPERADMIN_PASSWORD})")
# Create superadmin permissions
perms = UserPermissions(user_id=user.id) perms = UserPermissions(user_id=user.id)
for field in PERMISSION_FIELDS: for field in PERMISSION_FIELDS:
if hasattr(perms, field): if hasattr(perms, field):
...@@ -34,7 +36,6 @@ def seed_superadmin(): ...@@ -34,7 +36,6 @@ def seed_superadmin():
db.commit() db.commit()
print(" Created superadmin permissions") print(" Created superadmin permissions")
else: else:
# Ensure superadmin has permissions row
sp = db.query(UserPermissions).filter(UserPermissions.user_id == existing.id).first() sp = db.query(UserPermissions).filter(UserPermissions.user_id == existing.id).first()
if not sp: if not sp:
perms = UserPermissions(user_id=existing.id) perms = UserPermissions(user_id=existing.id)
...@@ -45,7 +46,7 @@ def seed_superadmin(): ...@@ -45,7 +46,7 @@ def seed_superadmin():
db.commit() db.commit()
print(" Created superadmin permissions (existing user)") 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() defaults = db.query(UserPermissions).filter(UserPermissions.user_id == "__defaults__").first()
if not defaults: if not defaults:
defaults = UserPermissions(user_id="__defaults__") defaults = UserPermissions(user_id="__defaults__")
...@@ -56,6 +57,14 @@ def seed_superadmin(): ...@@ -56,6 +57,14 @@ def seed_superadmin():
db.commit() db.commit()
print(" Created default permissions template") 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: except Exception as e:
print(f" Seed error: {e}") print(f" Seed error: {e}")
finally: finally:
......
...@@ -5,117 +5,252 @@ function headers(token) { ...@@ -5,117 +5,252 @@ function headers(token) {
if (token) h["Authorization"] = `Bearer ${token}`; if (token) h["Authorization"] = `Bearer ${token}`;
return h; 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) { async function request(method, path, token, body) {
const opts = { method, headers: headers(token) }; const opts = { method, headers: headers(token) };
if (body) opts.body = JSON.stringify(body); if (body) opts.body = JSON.stringify(body);
const res = await fetch(`${BASE}${path}`, opts); 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(); return res.json();
} }
// Auth // ═══════════════════════════════════════
export const login = (u, p) => request("POST", "/auth/login", null, { username: u, password: p }); // Auth
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);
export const getRegistrationStatus = () => request("GET", "/auth/registration-status", null);
// Chats
export const listChats = (t) => request("GET", "/chats", t); export const login = (username, password) =>
export const createChat = (t, d = {}) => request("POST", "/chats", t, d); request("POST", "/auth/login", null, { username, password });
export const updateChat = (t, id, d) => request("PUT", `/chats/${id}`, t, d);
export const renameChat = (t, id, title) => updateChat(t, id, { title }); export const register = (username, email, password) =>
export const deleteChat = (t, id) => request("DELETE", `/chats/${id}`, t); request("POST", "/auth/register", null, { username, email, password });
export const getMessages = (t, id) => request("GET", `/chats/${id}/messages`, t);
export const checkGenerating = (t, id) => request("GET", `/chats/${id}/generating`, t); export const getMe = (token) => request("GET", "/auth/me", token);
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); // ═══════════════════════════════════════
// Chats
// Streaming // ═══════════════════════════════════════
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) { 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 }); const res = await fetch(`${BASE}/chats/${chatId}/messages`, {
if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(extractError(err, "Stream failed")); } method: "POST", headers: headers(token),
const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; body: JSON.stringify(body), signal,
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 { } } 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) { export async function exportPptx(token, markdown, title) {
const res = await fetch(`${BASE}/export/pptx`, { method: "POST", headers: headers(token), body: JSON.stringify({ markdown, title }) }); const res = await fetch(`${BASE}/export/pptx`, {
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "PPTX export failed")); } method: "POST", headers: headers(token),
const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; body: JSON.stringify({ markdown, title }),
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); 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) { export async function exportDocx(token, markdown, title) {
const res = await fetch(`${BASE}/export/docx`, { method: "POST", headers: headers(token), body: JSON.stringify({ markdown, title }) }); const res = await fetch(`${BASE}/export/docx`, {
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "DOCX export failed")); } method: "POST", headers: headers(token),
const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; body: JSON.stringify({ markdown, title }),
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); 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; // GitLab
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 = (token) => request("GET", "/gitlab/settings", token);
export const gitlabGetSettings = (t) => request("GET", "/gitlab/settings", t); export const gitlabUpdateSettings = (token, data) => request("PUT", "/gitlab/settings", token, data);
export const gitlabUpdateSettings = (t, d) => request("PUT", "/gitlab/settings", t, d); export const gitlabTestConnection = (token) => request("POST", "/gitlab/test-connection", token);
export const gitlabTestConnection = (t) => request("POST", "/gitlab/test-connection", t); export const gitlabSearchProjects = (token, search, owned) => request("GET", `/gitlab/projects?search=${encodeURIComponent(search || "")}&owned=${owned || false}`, token);
export const gitlabSearchProjects = (t, s, o) => request("GET", `/gitlab/projects?search=${encodeURIComponent(s || "")}&owned=${o || false}`, t); export const gitlabCreateProject = (token, data) => request("POST", "/gitlab/projects", token, data);
export const gitlabCreateProject = (t, d) => request("POST", "/gitlab/projects", t, d); export const gitlabListRepos = (token) => request("GET", "/gitlab/repos", token);
export const gitlabListRepos = (t) => request("GET", "/gitlab/repos", t); export const gitlabLinkRepo = (token, projectId) => request("POST", "/gitlab/repos", token, { gitlab_project_id: projectId });
export const gitlabLinkRepo = (t, pid) => request("POST", "/gitlab/repos", t, { gitlab_project_id: pid }); export const gitlabUnlinkRepo = (token, repoId) => request("DELETE", `/gitlab/repos/${repoId}`, token);
export const gitlabUnlinkRepo = (t, id) => request("DELETE", `/gitlab/repos/${id}`, t); export const gitlabGetTree = (token, repoId, path, ref) => request("GET", `/gitlab/repos/${repoId}/tree?path=${encodeURIComponent(path || "")}&ref=${encodeURIComponent(ref || "")}`, token);
export const gitlabGetTree = (t, id, p, r) => request("GET", `/gitlab/repos/${id}/tree?path=${encodeURIComponent(p || "")}&ref=${encodeURIComponent(r || "")}`, t); export const gitlabGetFile = (token, repoId, path, ref) => request("GET", `/gitlab/repos/${repoId}/file?path=${encodeURIComponent(path)}&ref=${encodeURIComponent(ref || "")}`, token);
export const gitlabGetFile = (t, id, p, r) => request("GET", `/gitlab/repos/${id}/file?path=${encodeURIComponent(p)}&ref=${encodeURIComponent(r || "")}`, t); export const gitlabGetBranches = (token, repoId) => request("GET", `/gitlab/repos/${repoId}/branches`, token);
export const gitlabGetBranches = (t, id) => request("GET", `/gitlab/repos/${id}/branches`, t); export const gitlabCommit = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/commit`, token, data);
export const gitlabCreateBranch = (t, id, d) => request("POST", `/gitlab/repos/${id}/branches`, t, d); export const gitlabCommitSingle = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/commit-single`, token, data);
export const gitlabCommit = (t, id, d) => request("POST", `/gitlab/repos/${id}/commit`, t, d); export const gitlabCreateBranch = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/branches`, token, data);
export const gitlabCommitSingle = (t, id, d) => request("POST", `/gitlab/repos/${id}/commit-single`, t, d); export const gitlabCreateMR = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/merge-request`, token, data);
export const gitlabCreateMR = (t, id, d) => request("POST", `/gitlab/repos/${id}/merge-request`, t, d); export const gitlabAnalyzeRepo = (token, repoId) => request("POST", `/gitlab/repos/${repoId}/analyze`, token);
export const gitlabReanalyzeRepo = (t, id) => request("POST", `/gitlab/repos/${id}/analyze`, t); export const gitlabGetRepoMap = (token, repoId) => request("GET", `/gitlab/repos/${repoId}/map`, token);
export const gitlabGetRepoMap = (t, id) => request("GET", `/gitlab/repos/${id}/map`, t); export const gitlabListActions = (token, status) => request("GET", `/gitlab/actions?status=${status || "pending"}`, token);
export const gitlabListActions = (t, s) => request("GET", `/gitlab/actions?status=${s || "pending"}`, t); export const gitlabCreateAction = (token, data) => request("POST", "/gitlab/actions", token, data);
export const gitlabCreateAction = (t, d) => request("POST", "/gitlab/actions", t, d); export const gitlabApproveAction = (token, actionId) => request("POST", `/gitlab/actions/${actionId}/approve`, token);
export const gitlabApproveAction = (t, id) => request("POST", `/gitlab/actions/${id}/approve`, t); export const gitlabRejectAction = (token, actionId) => request("POST", `/gitlab/actions/${actionId}/reject`, token);
export const gitlabRejectAction = (t, id) => request("POST", `/gitlab/actions/${id}/reject`, t); \ No newline at end of file
\ No newline at end of file
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useApp } from "../store"; import { useApp } from "../store";
import { import {
adminStats, adminListUsers, adminCreateUser, adminUpdateUser, adminDeleteUser, adminStats, adminListUsers, adminCreateUser, adminUpdateUser, adminDeleteUser,
adminListChats, adminGetAppSettings, adminUpdateAppSettings,
adminGetPermissionDefaults, adminUpdatePermissionDefaults, adminApplyDefaults,
adminGetUserPermissions, adminUpdateUserPermissions, adminGetUserPermissions, adminUpdateUserPermissions,
adminGetDefaultPermissions, adminUpdateDefaultPermissions, adminApplyDefaults,
} from "../api"; } from "../api";
import { import {
ArrowLeft, Users, MessageSquare, Database, Zap, Users, MessageSquare, Brain, Database, Shield, Plus, Trash2, Edit, Check, X,
UserPlus, Trash2, Shield, ShieldOff, Save, X, BarChart3, Settings2, ChevronDown, ChevronRight, ToggleLeft, ToggleRight, UserPlus,
Settings2, Check, Globe, Paintbrush, BookOpen, GitBranch, RefreshCw, Lock, Unlock, Save,
Paperclip, Presentation, FileOutput, Cpu, Gauge, Lock,
Loader2, RotateCcw, Copy,
} from "lucide-react"; } 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() { export default function AdminPage() {
const { state } = useApp(); const { state } = useApp();
const navigate = useNavigate(); const [tab, setTab] = useState("stats");
const [stats, setStats] = useState(null); const [stats, setStats] = useState(null);
const [users, setUsers] = useState([]); 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 [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 [newUser, setNewUser] = useState({ username: "", email: "", password: "", role: "user", quota_tokens_monthly: 2000000 });
const [error, setError] = useState("");
// Permissions modal // Edit user
const [permsUser, setPermsUser] = useState(null); // user object or { id: "__defaults__", username: "Default Template" } const [editUserId, setEditUserId] = useState(null);
const [permsData, setPermsData] = useState(null); const [editData, setEditData] = useState({});
const [permsSaving, setPermsSaving] = useState(false);
const [permsLoading, setPermsLoading] = useState(false); // Permissions
const [applyingDefaults, setApplyingDefaults] = useState(false); const [permUserId, setPermUserId] = useState(null);
const [permData, setPermData] = useState(null);
const [defaults, setDefaults] = useState(null);
const token = state.token;
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true);
setError("");
try { try {
const [s, u] = await Promise.all([adminStats(state.token), adminListUsers(state.token)]); const [s, u, c, as_] = await Promise.all([
setStats(s); setUsers(u); adminStats(token), adminListUsers(token), adminListChats(token), adminGetAppSettings(token),
} catch (err) { setError(err.message); } ]);
}, [state.token]); setStats(s); setUsers(u); setChats(c); setAppSettings(as_);
} catch (e) { setError(e.message); } finally { setLoading(false); }
}, [token]);
useEffect(() => { load(); }, [load]); useEffect(() => { load(); }, [load]);
async function handleCreate(e) { function flash(msg) { setSuccess(msg); setTimeout(() => setSuccess(""), 3000); }
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); }
}
async function handleDelete(userId, username) { async function toggleRegistration() {
if (!confirm(`Delete user "${username}"? This is permanent.`)) return; try {
try { await adminDeleteUser(state.token, userId); load(); } catch (err) { setError(err.message); } 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 handleCreateUser(e) {
async function openPerms(user) { e.preventDefault();
setPermsUser(user); setPermsLoading(true); setPermsData(null);
try { try {
const data = user.id === "__defaults__" await adminCreateUser(token, newUser);
? await adminGetDefaultPermissions(state.token) setShowCreate(false); setNewUser({ username: "", email: "", password: "", role: "user", quota_tokens_monthly: 2000000 });
: await adminGetUserPermissions(state.token, user.id); flash("User created"); load();
setPermsData(data); } catch (e) { setError(e.message); }
} catch (err) { setError(err.message); setPermsUser(null); }
setPermsLoading(false);
} }
async function savePerms() { async function handleUpdateUser(userId) {
if (!permsUser || !permsData) return;
setPermsSaving(true);
try { try {
if (permsUser.id === "__defaults__") { await adminUpdateUser(token, userId, editData);
await adminUpdateDefaultPermissions(state.token, permsData); setEditUserId(null); flash("User updated"); load();
} else { } catch (e) { setError(e.message); }
await adminUpdateUserPermissions(state.token, permsUser.id, permsData);
}
setPermsUser(null); setPermsData(null);
} catch (err) { setError(err.message); }
setPermsSaving(false);
} }
async function handleApplyDefaults() { async function handleDeleteUser(userId, username) {
if (!confirm("Apply default permissions to ALL regular users? This will overwrite their current permissions.")) return; if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return;
setApplyingDefaults(true); try { await adminDeleteUser(token, userId); flash("User deleted"); load(); } catch (e) { setError(e.message); }
try {
const res = await adminApplyDefaults(state.token);
alert(`✅ Applied to ${res.users_updated} users`);
} catch (err) { setError(err.message); }
setApplyingDefaults(false);
} }
function updatePerm(key, value) { async function handleToggleActive(userId, currentActive) {
setPermsData(prev => ({ ...prev, [key]: value })); try { await adminUpdateUser(token, userId, { is_active: !currentActive }); flash("User updated"); load(); } catch (e) { setError(e.message); }
} }
function formatNum(n) { async function openPermissions(userId) {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"; try {
if (n >= 1_000) return (n / 1_000).toFixed(0) + "K"; const [p, d] = await Promise.all([adminGetUserPermissions(token, userId), adminGetPermissionDefaults(token)]);
return String(n); setPermUserId(userId); setPermData(p); setDefaults(d);
} catch (e) { setError(e.message); }
} }
if (state.user?.role !== "superadmin") { async function savePermissions() {
return <div className="h-full flex items-center justify-center"><p className="text-anton-danger text-lg">⛔ Access Denied</p></div>; 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 ( return (
<div className="h-full overflow-y-auto bg-anton-bg p-4 sm:p-6"> <div className="flex-1 flex flex-col min-h-0 bg-anton-bg">
<div className="max-w-6xl mx-auto space-y-6 animate-fade-in"> {/* Header */}
{/* Header */} <div className="border-b border-anton-border bg-anton-surface px-6 py-4">
<div className="flex items-center gap-4"> <div className="flex items-center justify-between">
<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> <div>
<h1 className="text-2xl font-bold text-white flex items-center gap-2"><Shield size={24} className="text-anton-accent" /> Admin Panel</h1> <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-anton-muted text-sm">Users, permissions & cost control</p> <p className="text-xs text-anton-muted mt-0.5">Superadmin Control Panel</p>
</div> </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>
</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 TAB ═══ */}
{stats && ( {tab === "stats" && stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <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: "Users", value: stats.total_users, icon: Users, color: "text-blue-400" },
{ label: "Chats", value: stats.total_chats, icon: MessageSquare, color: "text-green-400" }, { label: "Active", value: stats.active_users, icon: Check, color: "text-green-400" },
{ label: "Messages", value: formatNum(stats.total_messages), icon: Zap, color: "text-anton-accent" }, { label: "Chats", value: stats.total_chats, icon: MessageSquare, color: "text-purple-400" },
{ label: "Tokens Used", value: formatNum(stats.total_tokens_used), icon: Database, color: "text-purple-400" }, { label: "Messages", value: stats.total_messages, icon: MessageSquare, color: "text-cyan-400" },
].map((s) => ( { label: "Tokens Used", value: (stats.total_tokens_used / 1000000).toFixed(1) + "M", icon: Brain, color: "text-yellow-400" },
<div key={s.label} className="bg-anton-surface border border-anton-border rounded-xl p-4"> { label: "Knowledge", value: stats.total_knowledge_bases, icon: Database, color: "text-orange-400" },
<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> ].map((s, i) => (
<p className="text-2xl font-bold text-white">{s.value}</p> <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>
))} ))}
</div> </div>
)} )}
{/* User Management */} {/* ═══ USERS TAB ═══ */}
<div className="bg-anton-surface border border-anton-border rounded-xl overflow-hidden"> {tab === "users" && (
<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 justify-between">
<div className="flex items-center gap-2"> <h2 className="text-lg font-semibold text-white">Users ({users.length})</h2>
<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"> <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">
<Lock size={14} /> Defaults <Plus size={14} /> Create User
</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"}
</button> </button>
</div> </div>
</div>
{showCreate && ( {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"> <form onSubmit={handleCreateUser} className="bg-anton-card border border-anton-border rounded-xl p-4 space-y-3 animate-fade-in">
<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" /> <div className="grid grid-cols-2 gap-3">
<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 value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} placeholder="Username" required
<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" /> 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"> <input value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} placeholder="Email" required type="email"
<option value="user">User</option><option value="admin">Admin</option><option value="superadmin">Superadmin</option> 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> <input value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} placeholder="Password" required type="password"
<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" /> 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> <select value={newUser.role} onChange={(e) => setNewUser({ ...newUser, role: e.target.value })}
</form> 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 className="overflow-x-auto"> </div>
<table className="w-full text-sm"> <div className="flex gap-2">
<thead><tr className="text-left text-anton-muted border-b border-anton-border"> <button type="submit" className="px-4 py-2 bg-anton-accent text-white rounded-lg text-sm">Create</button>
<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> <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>
</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>
</div> </div>
</form>
)}
{/* Model Access */} <div className="space-y-2">
<div> {users.map((u) => (
<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 key={u.id} className="bg-anton-card border border-anton-border rounded-xl p-4 flex items-center gap-4">
<div className="space-y-2"> <div className="flex-1 min-w-0">
<label className="flex items-center gap-3 bg-anton-card rounded-lg px-4 py-3 border border-anton-border cursor-pointer"> <div className="flex items-center gap-2">
<input type="checkbox" checked={permsData.allowed_models === "all"} onChange={e => updatePerm("allowed_models", e.target.checked ? "all" : MODELS.map(m => m.id).join(","))} <span className="text-white font-medium text-sm">{u.username}</span>
className="w-4 h-4 rounded accent-anton-accent" /> <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>
<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> {!u.is_active && <span className="text-[10px] px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">disabled</span>}
</label> </div>
{permsData.allowed_models !== "all" && ( <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 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> </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>
))}
</div>
{/* Limits */} {/* Permissions Modal */}
<div> {permUserId && permData && (
<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="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4" onClick={() => setPermUserId(null)}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <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()}>
{LIMIT_DEFS.map(l => ( <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 key={l.key} className="bg-anton-card rounded-lg px-4 py-3 border border-anton-border"> <div className="space-y-3">
<div className="flex justify-between items-center mb-1"> {["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 className="text-xs text-anton-muted">{l.label}</label> <label key={key} className="flex items-center justify-between">
<span className="text-xs text-white font-mono">{(permsData[l.key] || 0) === 0 && l.desc?.includes("unlimited") ? "∞" : (permsData[l.key] || 0).toLocaleString()}</span> <span className="text-sm text-white">{key.replace(/^can_/, "").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}</span>
</div> <button onClick={() => setPermData({ ...permData, [key]: !permData[key] })}
<input type="number" min={l.min} max={l.max} step={l.step} value={permsData[l.key] || 0} className={`transition ${permData[key] ? "text-green-400" : "text-anton-muted"}`}>
onChange={e => updatePerm(l.key, Math.min(Math.max(Number(e.target.value) || 0, l.min), l.max))} {permData[key] ? <ToggleRight size={24} /> : <ToggleLeft size={24} />}
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" </button>
/> </label>
<p className="text-[9px] text-anton-muted mt-1">{l.desc}</p>
</div>
))} ))}
<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> </div>
</div> </div>
) : null} )}
</>
)}
{/* Modal Footer */} {/* ═══ SETTINGS TAB ═══ */}
{permsData && ( {tab === "settings" && (
<div className="px-6 py-4 border-t border-anton-border flex items-center justify-between"> <div className="space-y-6">
<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> {/* Registration Toggle */}
<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"> <div className="bg-anton-card border border-anton-border rounded-xl p-6">
{permsSaving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />} Save Permissions <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>
</button> <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> )}
)} </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 { useApp } from "../store";
import { login, register } from "../api"; import { login, register, getRegistrationStatus } from "../api";
import { Flame, Eye, EyeOff, Loader2 } from "lucide-react"; import { Flame, LogIn, UserPlus, Eye, EyeOff, AlertCircle } from "lucide-react";
export default function LoginPage() { export default function LoginPage() {
const { dispatch } = useApp(); const { dispatch } = useApp();
const [isRegister, setIsRegister] = useState(false); const [isLogin, setIsLogin] = useState(true);
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 [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); 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) { async function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
setError(""); setError("");
setLoading(true); setLoading(true);
try { try {
const res = isRegister let data;
? await register(username, email, password) if (isLogin) {
: await login(username, password); data = await login(username, password);
dispatch({ type: "LOGIN", token: res.token, user: res.user }); } else {
data = await register(username, email, password);
}
dispatch({ type: "LOGIN", token: data.token, user: data.user });
} catch (err) { } catch (err) {
setError(err.message || "Authentication failed"); setError(err.message || "Something went wrong");
} finally { } finally {
setLoading(false); 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="min-h-screen bg-anton-bg flex items-center justify-center p-4">
<div className="w-full max-w-sm"> <div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8"> <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"> <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-3xl 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 mt-1 text-sm">Avatar of All Elements of Code</p>
</div> </div>
{/* Form */} <div className="bg-anton-card border border-anton-border rounded-2xl p-6 shadow-xl">
<form onSubmit={handleSubmit} className="space-y-4"> {!checkingRegistration && registrationEnabled && (
<div> <div className="flex mb-6 bg-anton-bg rounded-xl p-1">
<label className="text-xs text-anton-muted mb-1.5 block">Username</label> <button onClick={() => { setIsLogin(true); setError(""); }}
<input 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"}`}>
type="text" Sign In
value={username} </button>
onChange={(e) => setUsername(e.target.value)} <button onClick={() => { setIsLogin(false); setError(""); }}
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={`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"}`}>
placeholder="Enter username" Register
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} />}
</button> </button>
</div> </div>
</div> )}
{error && ( {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} {error}
</div> </div>
)} )}
<button <form onSubmit={handleSubmit} className="space-y-4">
type="submit" <div>
disabled={loading} <label className="text-xs text-anton-muted mb-1 block">Username</label>
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" <input type="text" value={username} onChange={(e) => setUsername(e.target.value)}
> placeholder="Enter username" required autoFocus
{loading && <Loader2 size={18} className="animate-spin" />} 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" />
{isRegister ? "Create Account" : "Sign In"} </div>
</button>
{!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 <p className="text-center text-anton-muted text-xs mt-6">
type="button" Created by Mahmoud Aglan — AL-Arcade
onClick={() => { setIsRegister(!isRegister); setError(""); }} </p>
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>
</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