Commit 91ada25a authored by Administrator's avatar Administrator

Update 6 files via Son of Anton

parent 3c51ab07
...@@ -48,12 +48,29 @@ def _run_migrations(): ...@@ -48,12 +48,29 @@ def _run_migrations():
from backend.models import ChatAttachment from backend.models import ChatAttachment
ChatAttachment.__table__.create(bind=engine, checkfirst=True) ChatAttachment.__table__.create(bind=engine, checkfirst=True)
# Create user_permissions table if missing
if "user_permissions" not in existing_tables: if "user_permissions" not in existing_tables:
from backend.models import UserPermissions from backend.models import UserPermissions
UserPermissions.__table__.create(bind=engine, checkfirst=True) UserPermissions.__table__.create(bind=engine, checkfirst=True)
print(" Created user_permissions table") print(" Created user_permissions table")
# ── App Settings table ──
if "app_settings" not in existing_tables:
from backend.models import AppSettings
AppSettings.__table__.create(bind=engine, checkfirst=True)
print(" Created app_settings table")
# Ensure default app_settings row exists
from backend.models import AppSettings
from backend.database import SessionLocal
_db = SessionLocal()
try:
if not _db.query(AppSettings).first():
_db.add(AppSettings(allow_registration=True))
_db.commit()
print(" Created default app_settings row")
finally:
_db.close()
for table_name in ["gitlab_settings", "linked_repos", "pending_actions"]: for table_name in ["gitlab_settings", "linked_repos", "pending_actions"]:
if table_name not in existing_tables: if table_name not in existing_tables:
print(f" Creating {table_name} table") print(f" Creating {table_name} table")
......
...@@ -54,7 +54,6 @@ class UserPermissions(Base): ...@@ -54,7 +54,6 @@ class UserPermissions(Base):
unique=True, nullable=False, index=True, unique=True, nullable=False, index=True,
) )
# Feature access
can_use_web_search = Column(Boolean, default=False) can_use_web_search = Column(Boolean, default=False)
can_use_ui_design = Column(Boolean, default=False) can_use_ui_design = Column(Boolean, default=False)
can_use_knowledge_base = Column(Boolean, default=True) can_use_knowledge_base = Column(Boolean, default=True)
...@@ -63,10 +62,8 @@ class UserPermissions(Base): ...@@ -63,10 +62,8 @@ class UserPermissions(Base):
can_export_pptx = Column(Boolean, default=True) can_export_pptx = Column(Boolean, default=True)
can_export_docx = Column(Boolean, default=True) can_export_docx = Column(Boolean, default=True)
# Model access — "all" or comma-separated model IDs
allowed_models = Column(Text, default="eu.anthropic.claude-haiku-4-5-20251001-v1:0") allowed_models = Column(Text, default="eu.anthropic.claude-haiku-4-5-20251001-v1:0")
# Limits (0 = unlimited for count-based limits)
max_tokens_cap = Column(Integer, default=4096) max_tokens_cap = Column(Integer, default=4096)
max_reasoning_budget = Column(Integer, default=0) max_reasoning_budget = Column(Integer, default=0)
max_chats = Column(Integer, default=50) max_chats = Column(Integer, default=50)
...@@ -81,6 +78,18 @@ class UserPermissions(Base): ...@@ -81,6 +78,18 @@ class UserPermissions(Base):
user = relationship("User", back_populates="permissions") user = relationship("User", back_populates="permissions")
# ═══════════════════════════════════════════════════════════
# App-wide Settings (singleton row)
# ═══════════════════════════════════════════════════════════
class AppSettings(Base):
__tablename__ = "app_settings"
id = Column(String(36), primary_key=True, default=new_id)
allow_registration = Column(Boolean, default=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Chat(Base): class Chat(Base):
__tablename__ = "chats" __tablename__ = "chats"
......
"""
Superadmin routes: user management, stats, permissions — v4.2.0
"""
from pydantic import BaseModel
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func
from backend.database import get_db
from backend.models import User, Chat, Message, KnowledgeBase, UserPermissions
from backend.auth import (
require_superadmin, hash_password, get_user_permissions,
ensure_user_permissions, get_default_permissions_template,
)
from backend.config import PERMISSION_FIELDS, DEFAULT_PERMISSIONS, SUPERADMIN_PERMISSIONS, AVAILABLE_MODELS
router = APIRouter()
class UpdateUserBody(BaseModel):
email: Optional[str] = None
role: Optional[str] = None
is_active: Optional[bool] = None
quota_tokens_monthly: Optional[int] = None
password: Optional[str] = None
class CreateUserBody(BaseModel):
username: str
email: str
password: str
role: str = "user"
quota_tokens_monthly: int = 2_000_000
class PermissionsBody(BaseModel):
can_use_web_search: Optional[bool] = None
can_use_ui_design: Optional[bool] = None
can_use_knowledge_base: Optional[bool] = None
can_use_gitlab: Optional[bool] = None
can_use_attachments: Optional[bool] = None
can_export_pptx: Optional[bool] = None
can_export_docx: Optional[bool] = None
allowed_models: Optional[str] = None
max_tokens_cap: Optional[int] = None
max_reasoning_budget: Optional[int] = None
max_chats: Optional[int] = None
max_messages_per_day: Optional[int] = None
max_knowledge_bases: Optional[int] = None
max_documents_per_kb: Optional[int] = None
max_attachment_size_mb: Optional[int] = None
max_attachments_per_message: Optional[int] = None
# ═══════════════════════════════════════════════════
# Stats & Users
# ═══════════════════════════════════════════════════
@router.get("/stats")
def get_stats(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
return {
"total_users": db.query(User).count(),
"active_users": db.query(User).filter(User.is_active == True).count(),
"total_chats": db.query(Chat).count(),
"total_messages": db.query(Message).count(),
"total_tokens_used": db.query(func.sum(User.tokens_used_this_month)).scalar() or 0,
"total_knowledge_bases": db.query(KnowledgeBase).count(),
}
@router.get("/users")
def list_users(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
users = db.query(User).order_by(User.created_at.desc()).all()
result = []
for u in users:
chat_count = db.query(Chat).filter(Chat.user_id == u.id).count()
result.append({
"id": u.id,
"username": u.username,
"email": u.email,
"role": u.role,
"is_active": u.is_active,
"quota_tokens_monthly": u.quota_tokens_monthly,
"tokens_used_this_month": u.tokens_used_this_month,
"chat_count": chat_count,
"created_at": str(u.created_at),
})
return result
@router.post("/users")
def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
if db.query(User).filter(
(User.username == body.username) | (User.email == body.email)
).first():
raise HTTPException(409, "Username or email taken")
user = User(
username=body.username,
email=body.email,
password_hash=hash_password(body.password),
role=body.role,
quota_tokens_monthly=body.quota_tokens_monthly,
)
db.add(user)
db.commit()
db.refresh(user)
# Auto-create permissions from defaults template
ensure_user_permissions(user.id, db)
return {"id": user.id, "username": user.username}
@router.put("/users/{user_id}")
def update_user(user_id: str, body: UpdateUserBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(404)
if body.email is not None:
user.email = body.email
if body.role is not None:
user.role = body.role
if body.is_active is not None:
user.is_active = body.is_active
if body.quota_tokens_monthly is not None:
user.quota_tokens_monthly = body.quota_tokens_monthly
if body.password:
user.password_hash = hash_password(body.password)
db.commit()
return {"ok": True}
@router.delete("/users/{user_id}")
def delete_user(user_id: str, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(404)
if user.role == "superadmin":
raise HTTPException(400, "Cannot delete superadmin")
db.delete(user)
db.commit()
return {"ok": True}
@router.get("/chats")
def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
chats = db.query(Chat).order_by(Chat.updated_at.desc()).limit(200).all()
result = []
for c in chats:
user = db.query(User).filter(User.id == c.user_id).first()
msg_count = db.query(Message).filter(Message.chat_id == c.id).count()
result.append({
"id": c.id,
"title": c.title,
"username": user.username if user else "?",
"message_count": msg_count,
"updated_at": str(c.updated_at),
})
return result
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
# PERMISSIONS MANAGEMENT # REGISTRATION TOGGLE (append to end of file)
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
@router.get("/models") from backend.models import AppSettings # add this import at top if not already there
def list_available_models(admin: User = Depends(require_superadmin)):
return AVAILABLE_MODELS
@router.get("/registration")
def get_registration_setting(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
@router.get("/permissions/defaults") settings = db.query(AppSettings).first()
def get_defaults(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)): if not settings:
return get_default_permissions_template(db) settings = AppSettings(allow_registration=True)
db.add(settings)
db.commit()
@router.put("/permissions/defaults") db.refresh(settings)
def update_defaults(body: PermissionsBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)): return {"allow_registration": settings.allow_registration}
template = db.query(UserPermissions).filter(UserPermissions.user_id == "__defaults__").first()
if not template:
template = UserPermissions(user_id="__defaults__") @router.put("/registration")
db.add(template) def set_registration_setting(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
_apply_permissions_body(template, body) settings = db.query(AppSettings).first()
db.commit() if not settings:
return get_default_permissions_template(db) settings = AppSettings(allow_registration=True)
db.add(settings)
db.commit()
@router.post("/permissions/apply-defaults") db.refresh(settings)
def apply_defaults_to_all(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)): # Toggle
"""Apply default permissions template to ALL non-superadmin users.""" settings.allow_registration = not settings.allow_registration
template = get_default_permissions_template(db)
users = db.query(User).filter(User.role != "superadmin").all()
count = 0
for user in users:
perms = db.query(UserPermissions).filter(UserPermissions.user_id == user.id).first()
if not perms:
perms = UserPermissions(user_id=user.id)
db.add(perms)
for field in PERMISSION_FIELDS:
if hasattr(perms, field):
setattr(perms, field, template.get(field))
count += 1
db.commit() db.commit()
return {"ok": True, "users_updated": count} return {"allow_registration": settings.allow_registration}
\ No newline at end of file
@router.get("/users/{user_id}/permissions")
def get_user_perms(user_id: str, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(404, "User not found")
return get_user_permissions(user_id, db)
@router.put("/users/{user_id}/permissions")
def update_user_perms(user_id: str, body: PermissionsBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(404, "User not found")
if user.role == "superadmin":
raise HTTPException(400, "Cannot modify superadmin permissions — they always have full access")
ensure_user_permissions(user_id, db)
perms = db.query(UserPermissions).filter(UserPermissions.user_id == user_id).first()
_apply_permissions_body(perms, body)
db.commit()
return get_user_permissions(user_id, db)
def _apply_permissions_body(perms: UserPermissions, body: PermissionsBody):
"""Apply non-None fields from the body to a permissions object."""
data = body.dict(exclude_none=True)
for field, value in data.items():
if hasattr(perms, field):
setattr(perms, field, value)
\ No newline at end of file
""" """
Authentication routes: register, login, profile — with permissions. Authentication routes: register, login, profile — with permissions + registration toggle.
""" """
from pydantic import BaseModel from pydantic import BaseModel
...@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status ...@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from backend.database import get_db from backend.database import get_db
from backend.models import User from backend.models import User, AppSettings
from backend.auth import ( from backend.auth import (
hash_password, verify_password, create_token, get_current_user, hash_password, verify_password, create_token, get_current_user,
get_user_permissions, ensure_user_permissions, get_user_permissions, ensure_user_permissions,
...@@ -28,8 +28,25 @@ class LoginBody(BaseModel): ...@@ -28,8 +28,25 @@ class LoginBody(BaseModel):
password: str password: str
@router.get("/config")
def auth_config(db: Session = Depends(get_db)):
"""Public endpoint — no auth needed. Returns registration status."""
settings = db.query(AppSettings).first()
return {
"allow_registration": settings.allow_registration if settings else True,
}
@router.post("/register") @router.post("/register")
def register(body: RegisterBody, db: Session = Depends(get_db)): def register(body: RegisterBody, db: Session = Depends(get_db)):
# Check if registration is enabled
app_settings = db.query(AppSettings).first()
if app_settings and not app_settings.allow_registration:
raise HTTPException(
status.HTTP_403_FORBIDDEN,
"Registration is currently disabled. Contact an administrator.",
)
if db.query(User).filter( if db.query(User).filter(
(User.username == body.username) | (User.email == body.email) (User.username == body.username) | (User.email == body.email)
).first(): ).first():
...@@ -46,7 +63,6 @@ def register(body: RegisterBody, db: Session = Depends(get_db)): ...@@ -46,7 +63,6 @@ def register(body: RegisterBody, db: Session = Depends(get_db)):
db.commit() db.commit()
db.refresh(user) db.refresh(user)
# Auto-create permissions from defaults template
ensure_user_permissions(user.id, db) ensure_user_permissions(user.id, db)
token = create_token(user.id, user.role) token = create_token(user.id, user.role)
......
const BASE = "/api"; // ── Registration toggle (append to end of file) ──
function headers(token) { export const getAuthConfig = () =>
const h = { "Content-Type": "application/json" }; fetch(`${BASE}/auth/config`).then((r) => r.json());
if (token) h["Authorization"] = `Bearer ${token}`;
return h;
}
function authHeader(token) { return token ? { Authorization: `Bearer ${token}` } : {}; }
function extractError(err, d) { let m = err.detail || err.message || d; if (Array.isArray(m)) return m.map(x => x.msg || JSON.stringify(x)).join(", "); if (typeof m === "object") return m.message || JSON.stringify(m); return String(m); }
async function request(method, path, token, body) { export const getRegistrationSetting = (token) =>
const opts = { method, headers: headers(token) }; request("GET", "/admin/registration", token);
if (body) opts.body = JSON.stringify(body);
const res = await fetch(`${BASE}${path}`, opts);
if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(extractError(err, "Request failed")); }
return res.json();
}
// Auth export const toggleRegistration = (token) =>
export const login = (u, p) => request("POST", "/auth/login", null, { username: u, password: p }); request("PUT", "/admin/registration", token);
export const register = (u, e, p) => request("POST", "/auth/register", null, { username: u, email: e, password: p }); \ No newline at end of file
export const getMe = (t) => request("GET", "/auth/me", t);
// Chats
export const listChats = (t) => request("GET", "/chats", t);
export const createChat = (t, d = {}) => request("POST", "/chats", t, d);
export const updateChat = (t, id, d) => request("PUT", `/chats/${id}`, t, d);
export const renameChat = (t, id, title) => updateChat(t, id, { title });
export const deleteChat = (t, id) => request("DELETE", `/chats/${id}`, t);
export const getMessages = (t, id) => request("GET", `/chats/${id}/messages`, t);
export const checkGenerating = (t, id) => request("GET", `/chats/${id}/generating`, t);
export const refreshRepoContext = (t, id) => request("POST", `/chats/${id}/refresh-repo`, t);
export const commitFromChat = (t, id, d) => request("POST", `/chats/${id}/commit`, t, d);
// Streaming
export async function* streamMessage(token, chatId, body, signal) {
const res = await fetch(`${BASE}/chats/${chatId}/messages`, { method: "POST", headers: headers(token), body: JSON.stringify(body), signal });
if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(extractError(err, "Stream failed")); }
const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = "";
while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split("\n\n"); buffer = parts.pop() || ""; for (const part of parts) { const line = part.trim(); if (line.startsWith("data: ")) { try { yield JSON.parse(line.slice(6)); } catch { } } } }
if (buffer.trim().startsWith("data: ")) { try { yield JSON.parse(buffer.trim().slice(6)); } catch { } }
}
// Attachments
export async function uploadAttachments(t, chatId, files) { const form = new FormData(); for (const f of files) form.append("files", f); const res = await fetch(`${BASE}/chats/${chatId}/attachments`, { method: "POST", headers: authHeader(t), body: form }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "Upload failed")); } return res.json(); }
export function getAttachmentUrl(id) { return `${BASE}/attachments/${id}/file`; }
export const deleteAttachment = (t, id) => request("DELETE", `/attachments/${id}`, t);
// Knowledge
export const listKnowledgeBases = (t) => request("GET", "/knowledge", t);
export const createKnowledgeBase = (t, n, d = "") => request("POST", "/knowledge", t, { name: n, description: d });
export const getKnowledgeBase = (t, id) => request("GET", `/knowledge/${id}`, t);
export const updateKnowledgeBase = (t, id, d) => request("PUT", `/knowledge/${id}`, t, d);
export const deleteKnowledgeBase = (t, id) => request("DELETE", `/knowledge/${id}`, t);
export const listKnowledgeDocuments = (t, id) => request("GET", `/knowledge/${id}/documents`, t);
export const deleteKnowledgeDocument = (t, kbId, docId) => request("DELETE", `/knowledge/${kbId}/documents/${docId}`, t);
export async function uploadDocuments(t, kbId, files) { const form = new FormData(); for (const f of files) form.append("files", f); const res = await fetch(`${BASE}/knowledge/${kbId}/upload`, { method: "POST", headers: authHeader(t), body: form }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "Upload failed")); } return res.json(); }
export const uploadDocument = (t, kbId, f) => uploadDocuments(t, kbId, [f]);
// Admin
export const adminStats = (t) => request("GET", "/admin/stats", t);
export const adminListUsers = (t) => request("GET", "/admin/users", t);
export const adminCreateUser = (t, d) => request("POST", "/admin/users", t, d);
export const adminUpdateUser = (t, id, d) => request("PUT", `/admin/users/${id}`, t, d);
export const adminDeleteUser = (t, id) => request("DELETE", `/admin/users/${id}`, t);
export const adminListChats = (t) => request("GET", "/admin/chats", t);
// Admin — Permissions
export const adminGetUserPermissions = (t, uid) => request("GET", `/admin/users/${uid}/permissions`, t);
export const adminUpdateUserPermissions = (t, uid, d) => request("PUT", `/admin/users/${uid}/permissions`, t, d);
export const adminGetDefaultPermissions = (t) => request("GET", "/admin/permissions/defaults", t);
export const adminUpdateDefaultPermissions = (t, d) => request("PUT", "/admin/permissions/defaults", t, d);
export const adminApplyDefaults = (t) => request("POST", "/admin/permissions/apply-defaults", t);
export const adminGetModels = (t) => request("GET", "/admin/models", t);
// Code Download
export async function downloadZip(t, md, title) { const res = await fetch(`${BASE}/files/download-zip`, { method: "POST", headers: headers(t), body: JSON.stringify({ markdown: md, title: title || null }) }); if (!res.ok) throw new Error("Download failed"); const ct = res.headers.get("content-type") || ""; if (ct.includes("application/zip")) { const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; const raw = (title || "").trim(); a.download = `${raw && raw !== "New Chat" ? raw.replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 60) || "code" : "code"}.zip`; a.click(); URL.revokeObjectURL(url); } else { const data = await res.json(); if (data.error) throw new Error(data.error); } }
// Export PPTX / DOCX
export async function exportPptx(token, markdown, title) {
const res = await fetch(`${BASE}/export/pptx`, { method: "POST", headers: headers(token), body: JSON.stringify({ markdown, title }) });
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "PPTX export failed")); }
const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url;
const safe = (title || "presentation").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 50) || "presentation";
a.download = `${safe}.pptx`; a.click(); URL.revokeObjectURL(url);
}
export async function exportDocx(token, markdown, title) {
const res = await fetch(`${BASE}/export/docx`, { method: "POST", headers: headers(token), body: JSON.stringify({ markdown, title }) });
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(extractError(err, "DOCX export failed")); }
const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url;
const safe = (title || "document").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 50) || "document";
a.download = `${safe}.docx`; a.click(); URL.revokeObjectURL(url);
}
// Utilities
const CODE_BLOCK_RE = /```(\S*?)(?::(\S+?))?\s*?\n([\s\S]*?)```/g;
export function extractCodeBlocks(md) { if (!md) return []; const blocks = []; let m; const re = new RegExp(CODE_BLOCK_RE.source, "g"); while ((m = re.exec(md)) !== null) { const lang = (m[1] || "text").toLowerCase(); const fn = m[2] || null; const code = (m[3] || "").trim(); if (code) blocks.push({ language: lang, filename: fn, code }); } return blocks; }
// GitLab
export const gitlabGetSettings = (t) => request("GET", "/gitlab/settings", t);
export const gitlabUpdateSettings = (t, d) => request("PUT", "/gitlab/settings", t, d);
export const gitlabTestConnection = (t) => request("POST", "/gitlab/test-connection", t);
export const gitlabSearchProjects = (t, s, o) => request("GET", `/gitlab/projects?search=${encodeURIComponent(s || "")}&owned=${o || false}`, t);
export const gitlabCreateProject = (t, d) => request("POST", "/gitlab/projects", t, d);
export const gitlabListRepos = (t) => request("GET", "/gitlab/repos", t);
export const gitlabLinkRepo = (t, pid) => request("POST", "/gitlab/repos", t, { gitlab_project_id: pid });
export const gitlabUnlinkRepo = (t, id) => request("DELETE", `/gitlab/repos/${id}`, t);
export const gitlabGetTree = (t, id, p, r) => request("GET", `/gitlab/repos/${id}/tree?path=${encodeURIComponent(p || "")}&ref=${encodeURIComponent(r || "")}`, t);
export const gitlabGetFile = (t, id, p, r) => request("GET", `/gitlab/repos/${id}/file?path=${encodeURIComponent(p)}&ref=${encodeURIComponent(r || "")}`, t);
export const gitlabGetBranches = (t, id) => request("GET", `/gitlab/repos/${id}/branches`, t);
export const gitlabCreateBranch = (t, id, d) => request("POST", `/gitlab/repos/${id}/branches`, t, d);
export const gitlabCommit = (t, id, d) => request("POST", `/gitlab/repos/${id}/commit`, t, d);
export const gitlabCommitSingle = (t, id, d) => request("POST", `/gitlab/repos/${id}/commit-single`, t, d);
export const gitlabCreateMR = (t, id, d) => request("POST", `/gitlab/repos/${id}/merge-request`, t, d);
export const gitlabReanalyzeRepo = (t, id) => request("POST", `/gitlab/repos/${id}/analyze`, t);
export const gitlabGetRepoMap = (t, id) => request("GET", `/gitlab/repos/${id}/map`, t);
export const gitlabListActions = (t, s) => request("GET", `/gitlab/actions?status=${s || "pending"}`, t);
export const gitlabCreateAction = (t, d) => request("POST", "/gitlab/actions", t, d);
export const gitlabApproveAction = (t, id) => request("POST", `/gitlab/actions/${id}/approve`, t);
export const gitlabRejectAction = (t, id) => request("POST", `/gitlab/actions/${id}/reject`, t);
\ No newline at end of file
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useApp } from "../store"; import { useApp } from "../store";
import { login, register } from "../api"; import { login, register, getAuthConfig } from "../api";
import { Flame, Eye, EyeOff, Loader2 } from "lucide-react"; import { Flame, LogIn, UserPlus, AlertCircle } from "lucide-react";
export default function LoginPage() { export default function LoginPage() {
const { dispatch } = useApp(); const { dispatch } = useApp();
...@@ -9,114 +9,136 @@ export default function LoginPage() { ...@@ -9,114 +9,136 @@ export default function LoginPage() {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [showPw, setShowPw] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [allowRegistration, setAllowRegistration] = useState(true);
const [configLoaded, setConfigLoaded] = useState(false);
useEffect(() => {
getAuthConfig()
.then((data) => {
setAllowRegistration(data.allow_registration !== false);
setConfigLoaded(true);
})
.catch(() => {
setAllowRegistration(true);
setConfigLoaded(true);
});
}, []);
async function handleSubmit(e) { async function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
setError(""); setError("");
setLoading(true); setLoading(true);
try { try {
const res = isRegister let result;
? await register(username, email, password) if (isRegister) {
: await login(username, password); if (!allowRegistration) {
dispatch({ type: "LOGIN", token: res.token, user: res.user }); setError("Registration is disabled.");
setLoading(false);
return;
}
result = await register(username, email, password);
} else {
result = await login(username, password);
}
dispatch({ type: "LOGIN", token: result.token, user: result.user });
} catch (err) { } catch (err) {
setError(err.message || "Authentication failed"); setError(err.message || "Something went wrong");
} 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="h-dvh flex items-center justify-center bg-anton-bg p-4">
<div className="w-full max-w-sm"> <div className="w-full max-w-sm">
{/* Logo */} <div className="flex flex-col items-center mb-8">
<div className="text-center mb-8"> <div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20 mb-4">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
<Flame size={32} className="text-white" /> <Flame size={32} className="text-white" />
</div> </div>
<h1 className="text-2xl font-bold text-white">Son of Anton</h1> <h1 className="text-2xl font-bold text-white">Son of Anton</h1>
<p className="text-anton-muted text-sm mt-1">Avatar of All Elements of Code</p> <p className="text-anton-muted text-sm mt-1">Avatar of All Elements of Code</p>
</div> </div>
{/* Form */} <div className="bg-anton-card border border-anton-border rounded-2xl p-6">
<form onSubmit={handleSubmit} className="space-y-4"> {/* Tab buttons — only show Register tab if registration is allowed */}
<div> <div className="flex gap-2 mb-6">
<label className="text-xs text-anton-muted mb-1.5 block">Username</label> <button
<input onClick={() => { setIsRegister(false); setError(""); }}
type="text" className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${
value={username} !isRegister ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"
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" <LogIn size={14} className="inline mr-1.5" />
required Login
autoComplete="username" </button>
autoCapitalize="off" {allowRegistration && (
/> <button
onClick={() => { setIsRegister(true); setError(""); }}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${
isRegister ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"
}`}
>
<UserPlus size={14} className="inline mr-1.5" />
Register
</button>
)}
</div> </div>
{isRegister && ( {error && (
<div className="mb-4 bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 flex items-start gap-2">
<AlertCircle size={14} className="text-red-400 mt-0.5 shrink-0" />
<span className="text-red-400 text-xs">{error}</span>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<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">Username</label>
<input <input
type="email" type="text" value={username}
value={email} onChange={(e) => setUsername(e.target.value)}
onChange={(e) => setEmail(e.target.value)} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent transition"
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" required autoComplete="username"
placeholder="your@email.com"
required
autoComplete="email"
/> />
</div> </div>
)}
<div> {isRegister && (
<label className="text-xs text-anton-muted mb-1.5 block">Password</label> <div>
<div className="relative"> <label className="text-xs text-anton-muted mb-1 block">Email</label>
<input
type="email" value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent transition"
required autoComplete="email"
/>
</div>
)}
<div>
<label className="text-xs text-anton-muted mb-1 block">Password</label>
<input <input
type={showPw ? "text" : "password"} type="password" value={password}
value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 pr-12 text-white focus:outline-none focus:border-anton-accent transition" className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent transition"
placeholder="••••••••" required autoComplete={isRegister ? "new-password" : "current-password"}
required
autoComplete={isRegister ? "new-password" : "current-password"}
/> />
<button
type="button"
onClick={() => setShowPw(!showPw)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition p-1"
>
{showPw ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
{error && (
<div className="bg-anton-danger/10 border border-anton-danger/30 text-anton-danger text-sm rounded-lg px-3 py-2.5">
{error}
</div> </div>
)}
<button <button
type="submit" type="submit" disabled={loading}
disabled={loading} className="w-full bg-anton-accent text-white py-2.5 rounded-lg font-medium hover:opacity-90 transition disabled:opacity-50"
className="w-full 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 ? "..." : isRegister ? "Create Account" : "Sign In"}
{loading && <Loader2 size={18} className="animate-spin" />} </button>
{isRegister ? "Create Account" : "Sign In"} </form>
</button>
<button {!allowRegistration && configLoaded && !isRegister && (
type="button" <p className="text-[11px] text-anton-muted text-center mt-4">
onClick={() => { setIsRegister(!isRegister); setError(""); }} Registration is currently disabled by the administrator.
className="w-full text-center text-sm text-anton-muted hover:text-white transition py-2" </p>
> )}
{isRegister ? "Already have an account? Sign in" : "Need an account? Register"} </div>
</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