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 // 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); 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 // 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 listChats = (token) => request("GET", "/chats", token);
export const renameChat = (t, id, title) => updateChat(t, id, { title });
export const deleteChat = (t, id) => request("DELETE", `/chats/${id}`, t); export const createChat = (token, data = {}) => request("POST", "/chats", token, data);
export const getMessages = (t, id) => request("GET", `/chats/${id}/messages`, t);
export const checkGenerating = (t, id) => request("GET", `/chats/${id}/generating`, t); export const updateChat = (token, chatId, data) =>
export const refreshRepoContext = (t, id) => request("POST", `/chats/${id}/refresh-repo`, t); request("PUT", `/chats/${chatId}`, token, data);
export const commitFromChat = (t, id, d) => request("POST", `/chats/${id}/commit`, t, d);
export const renameChat = (token, chatId, title) =>
// Streaming 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 // 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); export async function uploadAttachments(token, chatId, files) {
const form = new FormData();
// Knowledge for (const file of files) form.append("files", file);
export const listKnowledgeBases = (t) => request("GET", "/knowledge", t); const res = await fetch(`${BASE}/chats/${chatId}/attachments`, {
export const createKnowledgeBase = (t, n, d = "") => request("POST", "/knowledge", t, { name: n, description: d }); method: "POST", headers: authHeader(token), body: form,
export const getKnowledgeBase = (t, id) => request("GET", `/knowledge/${id}`, t); });
export const updateKnowledgeBase = (t, id, d) => request("PUT", `/knowledge/${id}`, t, d); if (!res.ok) {
export const deleteKnowledgeBase = (t, id) => request("DELETE", `/knowledge/${id}`, t); const err = await res.json().catch(() => ({}));
export const listKnowledgeDocuments = (t, id) => request("GET", `/knowledge/${id}/documents`, t); throw new Error(err.detail || "Upload failed");
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(); } return res.json();
export const uploadDocument = (t, kbId, f) => uploadDocuments(t, kbId, [f]); }
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 // 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 adminStats = (token) => request("GET", "/admin/stats", token);
export const adminUpdateUser = (t, id, d) => request("PUT", `/admin/users/${id}`, t, d); export const adminListUsers = (token) => request("GET", "/admin/users", token);
export const adminDeleteUser = (t, id) => request("DELETE", `/admin/users/${id}`, t); export const adminCreateUser = (token, data) => request("POST", "/admin/users", token, data);
export const adminListChats = (t) => request("GET", "/admin/chats", t); export const adminUpdateUser = (token, userId, data) => request("PUT", `/admin/users/${userId}`, token, data);
export const adminDeleteUser = (token, userId) => request("DELETE", `/admin/users/${userId}`, token);
// Admin — Permissions export const adminListChats = (token) => request("GET", "/admin/chats", token);
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 adminGetAppSettings = (token) => request("GET", "/admin/app-settings", token);
export const adminGetDefaultPermissions = (t) => request("GET", "/admin/permissions/defaults", t); export const adminUpdateAppSettings = (token, data) => request("PUT", "/admin/app-settings", token, data);
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 adminGetPermissionDefaults = (token) => request("GET", "/admin/permissions/defaults", token);
export const adminGetModels = (t) => request("GET", "/admin/models", t); export const adminUpdatePermissionDefaults = (token, data) => request("PUT", "/admin/permissions/defaults", token, data);
export const adminApplyDefaults = (token) => request("POST", "/admin/permissions/apply-defaults", token);
// Code Download export const adminGetUserPermissions = (token, userId) => request("GET", `/admin/users/${userId}/permissions`, token);
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 const adminUpdateUserPermissions = (token, userId, data) => request("PUT", `/admin/users/${userId}/permissions`, token, data);
export const adminListModels = (token) => request("GET", "/admin/models", token);
// Export PPTX / DOCX
// ═══════════════════════════════════════
// 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);
}
}
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;
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 // 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 gitlabGetSettings = (token) => request("GET", "/gitlab/settings", token);
export const gitlabSearchProjects = (t, s, o) => request("GET", `/gitlab/projects?search=${encodeURIComponent(s || "")}&owned=${o || false}`, t); export const gitlabUpdateSettings = (token, data) => request("PUT", "/gitlab/settings", token, data);
export const gitlabCreateProject = (t, d) => request("POST", "/gitlab/projects", t, d); export const gitlabTestConnection = (token) => request("POST", "/gitlab/test-connection", token);
export const gitlabListRepos = (t) => request("GET", "/gitlab/repos", t); export const gitlabSearchProjects = (token, search, owned) => request("GET", `/gitlab/projects?search=${encodeURIComponent(search || "")}&owned=${owned || false}`, token);
export const gitlabLinkRepo = (t, pid) => request("POST", "/gitlab/repos", t, { gitlab_project_id: pid }); export const gitlabCreateProject = (token, data) => request("POST", "/gitlab/projects", token, data);
export const gitlabUnlinkRepo = (t, id) => request("DELETE", `/gitlab/repos/${id}`, t); export const gitlabListRepos = (token) => request("GET", "/gitlab/repos", token);
export const gitlabGetTree = (t, id, p, r) => request("GET", `/gitlab/repos/${id}/tree?path=${encodeURIComponent(p || "")}&ref=${encodeURIComponent(r || "")}`, t); export const gitlabLinkRepo = (token, projectId) => request("POST", "/gitlab/repos", token, { gitlab_project_id: projectId });
export const gitlabGetFile = (t, id, p, r) => request("GET", `/gitlab/repos/${id}/file?path=${encodeURIComponent(p)}&ref=${encodeURIComponent(r || "")}`, t); export const gitlabUnlinkRepo = (token, repoId) => request("DELETE", `/gitlab/repos/${repoId}`, token);
export const gitlabGetBranches = (t, id) => request("GET", `/gitlab/repos/${id}/branches`, t); export const gitlabGetTree = (token, repoId, path, ref) => request("GET", `/gitlab/repos/${repoId}/tree?path=${encodeURIComponent(path || "")}&ref=${encodeURIComponent(ref || "")}`, token);
export const gitlabCreateBranch = (t, id, d) => request("POST", `/gitlab/repos/${id}/branches`, t, d); export const gitlabGetFile = (token, repoId, path, ref) => request("GET", `/gitlab/repos/${repoId}/file?path=${encodeURIComponent(path)}&ref=${encodeURIComponent(ref || "")}`, token);
export const gitlabCommit = (t, id, d) => request("POST", `/gitlab/repos/${id}/commit`, t, d); export const gitlabGetBranches = (token, repoId) => request("GET", `/gitlab/repos/${repoId}/branches`, token);
export const gitlabCommitSingle = (t, id, d) => request("POST", `/gitlab/repos/${id}/commit-single`, t, d); export const gitlabCommit = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/commit`, token, data);
export const gitlabCreateMR = (t, id, d) => request("POST", `/gitlab/repos/${id}/merge-request`, t, d); export const gitlabCommitSingle = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/commit-single`, token, data);
export const gitlabReanalyzeRepo = (t, id) => request("POST", `/gitlab/repos/${id}/analyze`, t); export const gitlabCreateBranch = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/branches`, token, data);
export const gitlabGetRepoMap = (t, id) => request("GET", `/gitlab/repos/${id}/map`, t); export const gitlabCreateMR = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/merge-request`, token, data);
export const gitlabListActions = (t, s) => request("GET", `/gitlab/actions?status=${s || "pending"}`, t); export const gitlabAnalyzeRepo = (token, repoId) => request("POST", `/gitlab/repos/${repoId}/analyze`, token);
export const gitlabCreateAction = (t, d) => request("POST", "/gitlab/actions", t, d); export const gitlabGetRepoMap = (token, repoId) => request("GET", `/gitlab/repos/${repoId}/map`, token);
export const gitlabApproveAction = (t, id) => request("POST", `/gitlab/actions/${id}/approve`, t); export const gitlabListActions = (token, status) => request("GET", `/gitlab/actions?status=${status || "pending"}`, token);
export const gitlabRejectAction = (t, id) => request("POST", `/gitlab/actions/${id}/reject`, t); export const gitlabCreateAction = (token, data) => request("POST", "/gitlab/actions", token, data);
\ No newline at end of file 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 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="flex items-center gap-4"> <div className="border-b border-anton-border bg-anton-surface px-6 py-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 items-center justify-between">
<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>
<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 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" />
<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> </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" /> </div>
<button type="submit" className="bg-anton-success text-white rounded-lg px-3 py-2 text-sm font-medium hover:opacity-90">Create</button> <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> </form>
)} )}
<div className="overflow-x-auto"> <div className="space-y-2">
<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) => ( {users.map((u) => (
<tr key={u.id} className="border-b border-anton-border/50 hover:bg-anton-card/50 transition"> <div key={u.id} className="bg-anton-card border border-anton-border rounded-xl p-4 flex items-center gap-4">
<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> <div className="flex-1 min-w-0">
<td className="px-5 py-3"> <div className="flex items-center gap-2">
{editId === u.id ? ( <span className="text-white font-medium text-sm">{u.username}</span>
<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"> <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>
<option value="user">user</option><option value="admin">admin</option><option value="superadmin">superadmin</option> {!u.is_active && <span className="text-[10px] px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">disabled</span>}
</select> </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>
<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> </div>
)}
</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" && ( {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> <div className="flex items-center gap-1">
)} <button onClick={() => handleToggleActive(u.id, u.is_active)} title={u.is_active ? "Disable" : "Enable"}
<button onClick={() => { setEditId(u.id); setEditData({}); }} className="p-1 rounded hover:bg-anton-accent/20 text-anton-accent text-xs">Edit</button> className="p-1.5 rounded-lg hover:bg-anton-bg transition text-anton-muted hover:text-white">
<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 ? <Unlock size={14} /> : <Lock size={14} />}
{u.is_active ? <ShieldOff size={14} /> : <Shield size={14} />}
</button> </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>} <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>
</td>
</tr>
))} ))}
</tbody>
</table>
</div> </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> </div>
<div className="flex gap-2 mt-4">
{/* ═══ PERMISSIONS MODAL ═══ */} <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>
{permsUser && ( <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 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> </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> </div>
</div>
)}
</>
)}
{permsLoading ? ( {/* ═══ SETTINGS TAB ═══ */}
<div className="flex items-center justify-center py-20"><Loader2 size={24} className="text-anton-accent animate-spin" /></div> {tab === "settings" && (
) : permsData ? ( <div className="space-y-6">
<div className="p-6 space-y-6 max-h-[70vh] overflow-y-auto"> {/* Registration Toggle */}
{/* Feature Access */} <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>
<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="text-sm text-white font-medium">
<div className="space-y-2"> Registration is {appSettings.registration_enabled ? <span className="text-green-400">ENABLED</span> : <span className="text-red-400">DISABLED</span>}
{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> </div>
<Toggle value={!!permsData[f.key]} onChange={v => updatePerm(f.key, v)} /> <div className="text-xs text-anton-muted mt-0.5">
</div> {appSettings.registration_enabled
); ? "Anyone can create a new account from the login page."
})} : "Only superadmins can create new user accounts."}
</div> </div>
</div> </div>
<button onClick={toggleRegistration}
{/* Model Access */} className={`transition-colors ${appSettings.registration_enabled ? "text-green-400 hover:text-green-300" : "text-anton-muted hover:text-white"}`}>
<div> {appSettings.registration_enabled ? <ToggleRight size={36} /> : <ToggleLeft size={36} />}
<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> </button>
<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>
)} )}
</div> </div>
</div>
{/* Limits */} {/* Recent Chats */}
<div> <div className="bg-anton-card border border-anton-border rounded-xl p-6">
<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> <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="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="space-y-2 max-h-80 overflow-y-auto">
{LIMIT_DEFS.map(l => ( {chats.slice(0, 50).map((c) => (
<div key={l.key} className="bg-anton-card rounded-lg px-4 py-3 border border-anton-border"> <div key={c.id} className="flex items-center justify-between bg-anton-bg border border-anton-border rounded-lg p-3">
<div className="flex justify-between items-center mb-1"> <div className="min-w-0">
<label className="text-xs text-anton-muted">{l.label}</label> <div className="text-sm text-white truncate">{c.title}</div>
<span className="text-xs text-white font-mono">{(permsData[l.key] || 0) === 0 && l.desc?.includes("unlimited") ? "∞" : (permsData[l.key] || 0).toLocaleString()}</span> <div className="text-xs text-anton-muted">{c.username}{c.message_count} msgs</div>
</div> </div>
<input type="number" min={l.min} max={l.max} step={l.step} value={permsData[l.key] || 0} <span className="text-[10px] text-anton-muted shrink-0">{new Date(c.updated_at).toLocaleDateString()}</span>
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> </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>
) : 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>
</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 className="bg-anton-card border border-anton-border rounded-2xl p-6 shadow-xl">
{!checkingRegistration && registrationEnabled && (
<div className="flex mb-6 bg-anton-bg rounded-xl p-1">
<button onClick={() => { setIsLogin(true); setError(""); }}
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition ${isLogin ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"}`}>
Sign In
</button>
<button onClick={() => { setIsLogin(false); setError(""); }}
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition ${!isLogin ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"}`}>
Register
</button>
</div>
)}
{error && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-2 text-red-400 text-sm">
<AlertCircle size={16} className="shrink-0" />
{error}
</div> </div>
)}
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="text-xs text-anton-muted mb-1.5 block">Username</label> <label className="text-xs text-anton-muted mb-1 block">Username</label>
<input <input type="text" value={username} onChange={(e) => setUsername(e.target.value)}
type="text" placeholder="Enter username" required autoFocus
value={username} 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" />
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> </div>
{isRegister && ( {!isLogin && (
<div> <div>
<label className="text-xs text-anton-muted mb-1.5 block">Email</label> <label className="text-xs text-anton-muted mb-1 block">Email</label>
<input <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
type="email" placeholder="Enter email" required
value={email} 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" />
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>
)} )}
<div> <div>
<label className="text-xs text-anton-muted mb-1.5 block">Password</label> <label className="text-xs text-anton-muted mb-1 block">Password</label>
<div className="relative"> <div className="relative">
<input <input type={showPassword ? "text" : "password"} value={password} onChange={(e) => setPassword(e.target.value)}
type={showPw ? "text" : "password"} placeholder="Enter password" required
value={password} 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" />
onChange={(e) => setPassword(e.target.value)} <button type="button" onClick={() => setShowPassword(!showPassword)}
className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 pr-12 text-white focus:outline-none focus:border-anton-accent transition" className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition">
placeholder="••••••••" {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
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> </div>
{error && ( <button type="submit" disabled={loading}
<div className="bg-anton-danger/10 border border-anton-danger/30 text-anton-danger text-sm rounded-lg px-3 py-2.5"> 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">
{error} {loading ? (
</div> <span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : isLogin ? (
<><LogIn size={18} /> Sign In</>
) : (
<><UserPlus size={18} /> Create Account</>
)} )}
<button
type="submit"
disabled={loading}
className="w-full py-3.5 bg-anton-accent text-white rounded-xl font-semibold hover:opacity-90 transition disabled:opacity-50 active:scale-[0.98] flex items-center justify-center gap-2"
>
{loading && <Loader2 size={18} className="animate-spin" />}
{isRegister ? "Create Account" : "Sign In"}
</button>
<button
type="button"
onClick={() => { setIsRegister(!isRegister); setError(""); }}
className="w-full text-center text-sm text-anton-muted hover:text-white transition py-2"
>
{isRegister ? "Already have an account? Sign in" : "Need an account? Register"}
</button> </button>
</form> </form>
{!checkingRegistration && !registrationEnabled && (
<div className="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg text-yellow-400 text-xs text-center">
Registration is currently disabled. Contact your administrator.
</div>
)}
</div>
<p className="text-center text-anton-muted text-xs mt-6">
Created by Mahmoud Aglan — AL-Arcade
</p>
</div> </div>
</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