Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
Son Of Anton
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Son Of Anton
Commits
49192084
Commit
49192084
authored
Apr 10, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 9 files via Son of Anton
parent
f96d6514
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
647 additions
and
488 deletions
+647
-488
config.py
backend/config.py
+5
-0
main.py
backend/main.py
+5
-1
models.py
backend/models.py
+8
-3
admin_routes.py
backend/routes/admin_routes.py
+37
-3
auth_routes.py
backend/routes/auth_routes.py
+19
-3
seed.py
backend/seed.py
+15
-6
api.js
frontend/src/api.js
+232
-97
AdminPage.jsx
frontend/src/pages/AdminPage.jsx
+230
-293
LoginPage.jsx
frontend/src/pages/LoginPage.jsx
+96
-82
No files found.
backend/config.py
View file @
49192084
...
@@ -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
# ═══════════════════════════════════════════════════
# ═══════════════════════════════════════════════════
...
...
backend/main.py
View file @
49192084
...
@@ -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"
)
...
...
backend/models.py
View file @
49192084
...
@@ -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"
...
...
backend/routes/admin_routes.py
View file @
49192084
"""
"""
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
# ═══════════════════════════════════════════════════
# ═══════════════════════════════════════════════════
...
...
backend/routes/auth_routes.py
View file @
49192084
"""
"""
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
)
...
...
backend/seed.py
View file @
49192084
"""
"""
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
:
...
...
frontend/src/api.js
View file @
49192084
...
@@ -5,117 +5,252 @@ function headers(token) {
...
@@ -5,117 +5,252 @@ function headers(token) {
if
(
token
)
h
[
"Authorization"
]
=
`Bearer
${
token
}
`
;
if
(
token
)
h
[
"Authorization"
]
=
`Bearer
${
token
}
`
;
return
h
;
return
h
;
}
}
function
authHeader
(
token
)
{
return
token
?
{
Authorization
:
`Bearer
${
token
}
`
}
:
{};
}
function
extractError
(
err
,
d
)
{
let
m
=
err
.
detail
||
err
.
message
||
d
;
if
(
Array
.
isArray
(
m
))
return
m
.
map
(
x
=>
x
.
msg
||
JSON
.
stringify
(
x
)).
join
(
", "
);
if
(
typeof
m
===
"object"
)
return
m
.
message
||
JSON
.
stringify
(
m
);
return
String
(
m
);
}
function
authHeader
(
token
)
{
return
token
?
{
Authorization
:
`Bearer
${
token
}
`
}
:
{};
}
async
function
request
(
method
,
path
,
token
,
body
)
{
async
function
request
(
method
,
path
,
token
,
body
)
{
const
opts
=
{
method
,
headers
:
headers
(
token
)
};
const
opts
=
{
method
,
headers
:
headers
(
token
)
};
if
(
body
)
opts
.
body
=
JSON
.
stringify
(
body
);
if
(
body
)
opts
.
body
=
JSON
.
stringify
(
body
);
const
res
=
await
fetch
(
`
${
BASE
}${
path
}
`
,
opts
);
const
res
=
await
fetch
(
`
${
BASE
}${
path
}
`
,
opts
);
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
extractError
(
err
,
"Request failed"
));
}
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
err
.
detail
||
err
.
message
||
"Request failed"
);
}
return
res
.
json
();
return
res
.
json
();
}
}
// Auth
// ═══════════════════════════════════════
export
const
login
=
(
u
,
p
)
=>
request
(
"POST"
,
"/auth/login"
,
null
,
{
username
:
u
,
password
:
p
});
// Auth
export
const
register
=
(
u
,
e
,
p
)
=>
request
(
"POST"
,
"/auth/register"
,
null
,
{
username
:
u
,
email
:
e
,
password
:
p
});
// ═══════════════════════════════════════
export
const
getMe
=
(
t
)
=>
request
(
"GET"
,
"/auth/me"
,
t
);
export
const
getRegistrationStatus
=
()
=>
request
(
"GET"
,
"/auth/registration-status"
,
null
);
// Chats
export
const
listChats
=
(
t
)
=>
request
(
"GET"
,
"/chats"
,
t
);
export
const
login
=
(
username
,
password
)
=>
export
const
createChat
=
(
t
,
d
=
{})
=>
request
(
"POST"
,
"/chats"
,
t
,
d
);
request
(
"POST"
,
"/auth/login"
,
null
,
{
username
,
password
});
export
const
updateChat
=
(
t
,
id
,
d
)
=>
request
(
"PUT"
,
`/chats/
${
id
}
`
,
t
,
d
);
export
const
renameChat
=
(
t
,
id
,
title
)
=>
updateChat
(
t
,
id
,
{
title
});
export
const
register
=
(
username
,
email
,
password
)
=>
export
const
deleteChat
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/chats/
${
id
}
`
,
t
);
request
(
"POST"
,
"/auth/register"
,
null
,
{
username
,
email
,
password
});
export
const
getMessages
=
(
t
,
id
)
=>
request
(
"GET"
,
`/chats/
${
id
}
/messages`
,
t
);
export
const
checkGenerating
=
(
t
,
id
)
=>
request
(
"GET"
,
`/chats/
${
id
}
/generating`
,
t
);
export
const
getMe
=
(
token
)
=>
request
(
"GET"
,
"/auth/me"
,
token
);
export
const
refreshRepoContext
=
(
t
,
id
)
=>
request
(
"POST"
,
`/chats/
${
id
}
/refresh-repo`
,
t
);
export
const
commitFromChat
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/chats/
${
id
}
/commit`
,
t
,
d
);
// ═══════════════════════════════════════
// Chats
// Streaming
// ═══════════════════════════════════════
export
const
listChats
=
(
token
)
=>
request
(
"GET"
,
"/chats"
,
token
);
export
const
createChat
=
(
token
,
data
=
{})
=>
request
(
"POST"
,
"/chats"
,
token
,
data
);
export
const
updateChat
=
(
token
,
chatId
,
data
)
=>
request
(
"PUT"
,
`/chats/
${
chatId
}
`
,
token
,
data
);
export
const
renameChat
=
(
token
,
chatId
,
title
)
=>
updateChat
(
token
,
chatId
,
{
title
});
export
const
deleteChat
=
(
token
,
chatId
)
=>
request
(
"DELETE"
,
`/chats/
${
chatId
}
`
,
token
);
export
const
getMessages
=
(
token
,
chatId
)
=>
request
(
"GET"
,
`/chats/
${
chatId
}
/messages`
,
token
);
export
async
function
*
streamMessage
(
token
,
chatId
,
body
,
signal
)
{
export
async
function
*
streamMessage
(
token
,
chatId
,
body
,
signal
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/messages`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
(
body
),
signal
});
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/messages`
,
{
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
extractError
(
err
,
"Stream failed"
));
}
method
:
"POST"
,
headers
:
headers
(
token
),
const
reader
=
res
.
body
.
getReader
();
const
decoder
=
new
TextDecoder
();
let
buffer
=
""
;
body
:
JSON
.
stringify
(
body
),
signal
,
while
(
true
)
{
const
{
done
,
value
}
=
await
reader
.
read
();
if
(
done
)
break
;
buffer
+=
decoder
.
decode
(
value
,
{
stream
:
true
});
const
parts
=
buffer
.
split
(
"
\n\n
"
);
buffer
=
parts
.
pop
()
||
""
;
for
(
const
part
of
parts
)
{
const
line
=
part
.
trim
();
if
(
line
.
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
line
.
slice
(
6
));
}
catch
{
}
}
}
}
});
if
(
buffer
.
trim
().
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
buffer
.
trim
().
slice
(
6
));
}
catch
{
}
}
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
err
.
detail
||
"Stream failed"
);
}
const
reader
=
res
.
body
.
getReader
();
const
decoder
=
new
TextDecoder
();
let
buffer
=
""
;
while
(
true
)
{
const
{
done
,
value
}
=
await
reader
.
read
();
if
(
done
)
break
;
buffer
+=
decoder
.
decode
(
value
,
{
stream
:
true
});
const
parts
=
buffer
.
split
(
"
\n\n
"
);
buffer
=
parts
.
pop
()
||
""
;
for
(
const
part
of
parts
)
{
const
line
=
part
.
trim
();
if
(
line
.
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
line
.
slice
(
6
));
}
catch
{
}
}
}
}
if
(
buffer
.
trim
().
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
buffer
.
trim
().
slice
(
6
));
}
catch
{
}
}
}
export
const
checkGenerating
=
(
token
,
chatId
)
=>
request
(
"GET"
,
`/chats/
${
chatId
}
/generating`
,
token
);
export
async
function
commitFromChat
(
token
,
chatId
,
data
)
{
return
request
(
"POST"
,
`/chats/
${
chatId
}
/commit`
,
token
,
data
);
}
export
const
refreshRepoContext
=
(
token
,
chatId
)
=>
request
(
"POST"
,
`/chats/
${
chatId
}
/refresh-repo`
,
token
);
// ═══════════════════════════════════════
// Attachments
// ═══════════════════════════════════════
export
async
function
uploadAttachments
(
token
,
chatId
,
files
)
{
const
form
=
new
FormData
();
for
(
const
file
of
files
)
form
.
append
(
"files"
,
file
);
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/attachments`
,
{
method
:
"POST"
,
headers
:
authHeader
(
token
),
body
:
form
,
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
err
.
detail
||
"Upload failed"
);
}
return
res
.
json
();
}
export
function
getAttachmentUrl
(
attachmentId
)
{
return
`
${
BASE
}
/attachments/
${
attachmentId
}
/file`
;
}
export
const
deleteAttachment
=
(
token
,
attachmentId
)
=>
request
(
"DELETE"
,
`/attachments/
${
attachmentId
}
`
,
token
);
// ═══════════════════════════════════════
// Knowledge Base
// ═══════════════════════════════════════
export
const
listKnowledgeBases
=
(
token
)
=>
request
(
"GET"
,
"/knowledge"
,
token
);
export
const
createKnowledgeBase
=
(
token
,
name
,
description
=
""
)
=>
request
(
"POST"
,
"/knowledge"
,
token
,
{
name
,
description
});
export
const
getKnowledgeBase
=
(
token
,
kbId
)
=>
request
(
"GET"
,
`/knowledge/
${
kbId
}
`
,
token
);
export
const
deleteKnowledgeBase
=
(
token
,
kbId
)
=>
request
(
"DELETE"
,
`/knowledge/
${
kbId
}
`
,
token
);
export
const
deleteKnowledgeDocument
=
(
token
,
kbId
,
docId
)
=>
request
(
"DELETE"
,
`/knowledge/
${
kbId
}
/documents/
${
docId
}
`
,
token
);
export
async
function
uploadDocuments
(
token
,
kbId
,
files
)
{
const
form
=
new
FormData
();
for
(
const
file
of
files
)
form
.
append
(
"files"
,
file
);
const
res
=
await
fetch
(
`
${
BASE
}
/knowledge/
${
kbId
}
/upload`
,
{
method
:
"POST"
,
headers
:
authHeader
(
token
),
body
:
form
,
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
err
.
detail
||
"Upload failed"
);
}
return
res
.
json
();
}
export
const
uploadDocument
=
(
token
,
kbId
,
file
)
=>
uploadDocuments
(
token
,
kbId
,
[
file
]);
// ═══════════════════════════════════════
// Admin
// ═══════════════════════════════════════
export
const
adminStats
=
(
token
)
=>
request
(
"GET"
,
"/admin/stats"
,
token
);
export
const
adminListUsers
=
(
token
)
=>
request
(
"GET"
,
"/admin/users"
,
token
);
export
const
adminCreateUser
=
(
token
,
data
)
=>
request
(
"POST"
,
"/admin/users"
,
token
,
data
);
export
const
adminUpdateUser
=
(
token
,
userId
,
data
)
=>
request
(
"PUT"
,
`/admin/users/
${
userId
}
`
,
token
,
data
);
export
const
adminDeleteUser
=
(
token
,
userId
)
=>
request
(
"DELETE"
,
`/admin/users/
${
userId
}
`
,
token
);
export
const
adminListChats
=
(
token
)
=>
request
(
"GET"
,
"/admin/chats"
,
token
);
export
const
adminGetAppSettings
=
(
token
)
=>
request
(
"GET"
,
"/admin/app-settings"
,
token
);
export
const
adminUpdateAppSettings
=
(
token
,
data
)
=>
request
(
"PUT"
,
"/admin/app-settings"
,
token
,
data
);
export
const
adminGetPermissionDefaults
=
(
token
)
=>
request
(
"GET"
,
"/admin/permissions/defaults"
,
token
);
export
const
adminUpdatePermissionDefaults
=
(
token
,
data
)
=>
request
(
"PUT"
,
"/admin/permissions/defaults"
,
token
,
data
);
export
const
adminApplyDefaults
=
(
token
)
=>
request
(
"POST"
,
"/admin/permissions/apply-defaults"
,
token
);
export
const
adminGetUserPermissions
=
(
token
,
userId
)
=>
request
(
"GET"
,
`/admin/users/
${
userId
}
/permissions`
,
token
);
export
const
adminUpdateUserPermissions
=
(
token
,
userId
,
data
)
=>
request
(
"PUT"
,
`/admin/users/
${
userId
}
/permissions`
,
token
,
data
);
export
const
adminListModels
=
(
token
)
=>
request
(
"GET"
,
"/admin/models"
,
token
);
// ═══════════════════════════════════════
// Files / Export
// ═══════════════════════════════════════
export
async
function
downloadZip
(
token
,
markdown
,
title
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/files/download-zip`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
,
title
}),
});
if
(
!
res
.
ok
)
throw
new
Error
(
"Download failed"
);
const
ct
=
res
.
headers
.
get
(
"content-type"
)
||
""
;
if
(
ct
.
includes
(
"application/zip"
))
{
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
a
.
download
=
"son-of-anton-code.zip"
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
else
{
const
data
=
await
res
.
json
();
if
(
data
.
error
)
throw
new
Error
(
data
.
error
);
}
}
}
// Attachments
export
async
function
uploadAttachments
(
t
,
chatId
,
files
)
{
const
form
=
new
FormData
();
for
(
const
f
of
files
)
form
.
append
(
"files"
,
f
);
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/attachments`
,
{
method
:
"POST"
,
headers
:
authHeader
(
t
),
body
:
form
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"Upload failed"
));
}
return
res
.
json
();
}
export
function
getAttachmentUrl
(
id
)
{
return
`
${
BASE
}
/attachments/
${
id
}
/file`
;
}
export
const
deleteAttachment
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/attachments/
${
id
}
`
,
t
);
// Knowledge
export
const
listKnowledgeBases
=
(
t
)
=>
request
(
"GET"
,
"/knowledge"
,
t
);
export
const
createKnowledgeBase
=
(
t
,
n
,
d
=
""
)
=>
request
(
"POST"
,
"/knowledge"
,
t
,
{
name
:
n
,
description
:
d
});
export
const
getKnowledgeBase
=
(
t
,
id
)
=>
request
(
"GET"
,
`/knowledge/
${
id
}
`
,
t
);
export
const
updateKnowledgeBase
=
(
t
,
id
,
d
)
=>
request
(
"PUT"
,
`/knowledge/
${
id
}
`
,
t
,
d
);
export
const
deleteKnowledgeBase
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/knowledge/
${
id
}
`
,
t
);
export
const
listKnowledgeDocuments
=
(
t
,
id
)
=>
request
(
"GET"
,
`/knowledge/
${
id
}
/documents`
,
t
);
export
const
deleteKnowledgeDocument
=
(
t
,
kbId
,
docId
)
=>
request
(
"DELETE"
,
`/knowledge/
${
kbId
}
/documents/
${
docId
}
`
,
t
);
export
async
function
uploadDocuments
(
t
,
kbId
,
files
)
{
const
form
=
new
FormData
();
for
(
const
f
of
files
)
form
.
append
(
"files"
,
f
);
const
res
=
await
fetch
(
`
${
BASE
}
/knowledge/
${
kbId
}
/upload`
,
{
method
:
"POST"
,
headers
:
authHeader
(
t
),
body
:
form
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"Upload failed"
));
}
return
res
.
json
();
}
export
const
uploadDocument
=
(
t
,
kbId
,
f
)
=>
uploadDocuments
(
t
,
kbId
,
[
f
]);
// Admin
export
const
adminStats
=
(
t
)
=>
request
(
"GET"
,
"/admin/stats"
,
t
);
export
const
adminListUsers
=
(
t
)
=>
request
(
"GET"
,
"/admin/users"
,
t
);
export
const
adminCreateUser
=
(
t
,
d
)
=>
request
(
"POST"
,
"/admin/users"
,
t
,
d
);
export
const
adminUpdateUser
=
(
t
,
id
,
d
)
=>
request
(
"PUT"
,
`/admin/users/
${
id
}
`
,
t
,
d
);
export
const
adminDeleteUser
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/admin/users/
${
id
}
`
,
t
);
export
const
adminListChats
=
(
t
)
=>
request
(
"GET"
,
"/admin/chats"
,
t
);
// Admin — Permissions
export
const
adminGetUserPermissions
=
(
t
,
uid
)
=>
request
(
"GET"
,
`/admin/users/
${
uid
}
/permissions`
,
t
);
export
const
adminUpdateUserPermissions
=
(
t
,
uid
,
d
)
=>
request
(
"PUT"
,
`/admin/users/
${
uid
}
/permissions`
,
t
,
d
);
export
const
adminGetDefaultPermissions
=
(
t
)
=>
request
(
"GET"
,
"/admin/permissions/defaults"
,
t
);
export
const
adminUpdateDefaultPermissions
=
(
t
,
d
)
=>
request
(
"PUT"
,
"/admin/permissions/defaults"
,
t
,
d
);
export
const
adminApplyDefaults
=
(
t
)
=>
request
(
"POST"
,
"/admin/permissions/apply-defaults"
,
t
);
export
const
adminGetModels
=
(
t
)
=>
request
(
"GET"
,
"/admin/models"
,
t
);
// Code Download
export
async
function
downloadZip
(
t
,
md
,
title
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/files/download-zip`
,
{
method
:
"POST"
,
headers
:
headers
(
t
),
body
:
JSON
.
stringify
({
markdown
:
md
,
title
:
title
||
null
})
});
if
(
!
res
.
ok
)
throw
new
Error
(
"Download failed"
);
const
ct
=
res
.
headers
.
get
(
"content-type"
)
||
""
;
if
(
ct
.
includes
(
"application/zip"
))
{
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
const
raw
=
(
title
||
""
).
trim
();
a
.
download
=
`
${
raw
&&
raw
!==
"New Chat"
?
raw
.
replace
(
/
[^\w\s
-
]
/g
,
""
).
trim
().
replace
(
/
\s
+/g
,
"-"
).
slice
(
0
,
60
)
||
"code"
:
"code"
}
.zip`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
else
{
const
data
=
await
res
.
json
();
if
(
data
.
error
)
throw
new
Error
(
data
.
error
);
}
}
// Export PPTX / DOCX
export
async
function
exportPptx
(
token
,
markdown
,
title
)
{
export
async
function
exportPptx
(
token
,
markdown
,
title
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/export/pptx`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
,
title
})
});
const
res
=
await
fetch
(
`
${
BASE
}
/export/pptx`
,
{
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"PPTX export failed"
));
}
method
:
"POST"
,
headers
:
headers
(
token
),
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
body
:
JSON
.
stringify
({
markdown
,
title
}),
const
safe
=
(
title
||
"presentation"
).
replace
(
/
[^\w\s
-
]
/g
,
""
).
trim
().
replace
(
/
\s
+/g
,
"-"
).
slice
(
0
,
50
)
||
"presentation"
;
});
a
.
download
=
`
${
safe
}
.pptx`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
if
(
!
res
.
ok
)
throw
new
Error
(
"Export failed"
);
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
a
.
download
=
(
title
||
"presentation"
)
+
".pptx"
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
}
export
async
function
exportDocx
(
token
,
markdown
,
title
)
{
export
async
function
exportDocx
(
token
,
markdown
,
title
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/export/docx`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
,
title
})
});
const
res
=
await
fetch
(
`
${
BASE
}
/export/docx`
,
{
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"DOCX export failed"
));
}
method
:
"POST"
,
headers
:
headers
(
token
),
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
body
:
JSON
.
stringify
({
markdown
,
title
}),
const
safe
=
(
title
||
"document"
).
replace
(
/
[^\w\s
-
]
/g
,
""
).
trim
().
replace
(
/
\s
+/g
,
"-"
).
slice
(
0
,
50
)
||
"document"
;
});
a
.
download
=
`
${
safe
}
.docx`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
if
(
!
res
.
ok
)
throw
new
Error
(
"Export failed"
);
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
a
.
download
=
(
title
||
"document"
)
+
".docx"
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
}
// Utilities
// ═══════════════════════════════════════
const
CODE_BLOCK_RE
=
/```
(\S
*
?)(?:
:
(\S
+
?))?\s
*
?\n([\s\S]
*
?)
```/g
;
// GitLab
export
function
extractCodeBlocks
(
md
)
{
if
(
!
md
)
return
[];
const
blocks
=
[];
let
m
;
const
re
=
new
RegExp
(
CODE_BLOCK_RE
.
source
,
"g"
);
while
((
m
=
re
.
exec
(
md
))
!==
null
)
{
const
lang
=
(
m
[
1
]
||
"text"
).
toLowerCase
();
const
fn
=
m
[
2
]
||
null
;
const
code
=
(
m
[
3
]
||
""
).
trim
();
if
(
code
)
blocks
.
push
({
language
:
lang
,
filename
:
fn
,
code
});
}
return
blocks
;
}
// ═══════════════════════════════════════
// GitLab
export
const
gitlabGetSettings
=
(
token
)
=>
request
(
"GET"
,
"/gitlab/settings"
,
token
);
export
const
gitlabGetSettings
=
(
t
)
=>
request
(
"GET"
,
"/gitlab/settings"
,
t
);
export
const
gitlabUpdateSettings
=
(
token
,
data
)
=>
request
(
"PUT"
,
"/gitlab/settings"
,
token
,
data
);
export
const
gitlabUpdateSettings
=
(
t
,
d
)
=>
request
(
"PUT"
,
"/gitlab/settings"
,
t
,
d
);
export
const
gitlabTestConnection
=
(
token
)
=>
request
(
"POST"
,
"/gitlab/test-connection"
,
token
);
export
const
gitlabTestConnection
=
(
t
)
=>
request
(
"POST"
,
"/gitlab/test-connection"
,
t
);
export
const
gitlabSearchProjects
=
(
token
,
search
,
owned
)
=>
request
(
"GET"
,
`/gitlab/projects?search=
${
encodeURIComponent
(
search
||
""
)}
&owned=
${
owned
||
false
}
`
,
token
);
export
const
gitlabSearchProjects
=
(
t
,
s
,
o
)
=>
request
(
"GET"
,
`/gitlab/projects?search=
${
encodeURIComponent
(
s
||
""
)}
&owned=
${
o
||
false
}
`
,
t
);
export
const
gitlabCreateProject
=
(
token
,
data
)
=>
request
(
"POST"
,
"/gitlab/projects"
,
token
,
data
);
export
const
gitlabCreateProject
=
(
t
,
d
)
=>
request
(
"POST"
,
"/gitlab/projects"
,
t
,
d
);
export
const
gitlabListRepos
=
(
token
)
=>
request
(
"GET"
,
"/gitlab/repos"
,
token
);
export
const
gitlabListRepos
=
(
t
)
=>
request
(
"GET"
,
"/gitlab/repos"
,
t
);
export
const
gitlabLinkRepo
=
(
token
,
projectId
)
=>
request
(
"POST"
,
"/gitlab/repos"
,
token
,
{
gitlab_project_id
:
projectId
});
export
const
gitlabLinkRepo
=
(
t
,
pid
)
=>
request
(
"POST"
,
"/gitlab/repos"
,
t
,
{
gitlab_project_id
:
pid
});
export
const
gitlabUnlinkRepo
=
(
token
,
repoId
)
=>
request
(
"DELETE"
,
`/gitlab/repos/
${
repoId
}
`
,
token
);
export
const
gitlabUnlinkRepo
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/gitlab/repos/
${
id
}
`
,
t
);
export
const
gitlabGetTree
=
(
token
,
repoId
,
path
,
ref
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
repoId
}
/tree?path=
${
encodeURIComponent
(
path
||
""
)}
&ref=
${
encodeURIComponent
(
ref
||
""
)}
`
,
token
);
export
const
gitlabGetTree
=
(
t
,
id
,
p
,
r
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
id
}
/tree?path=
${
encodeURIComponent
(
p
||
""
)}
&ref=
${
encodeURIComponent
(
r
||
""
)}
`
,
t
);
export
const
gitlabGetFile
=
(
token
,
repoId
,
path
,
ref
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
repoId
}
/file?path=
${
encodeURIComponent
(
path
)}
&ref=
${
encodeURIComponent
(
ref
||
""
)}
`
,
token
);
export
const
gitlabGetFile
=
(
t
,
id
,
p
,
r
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
id
}
/file?path=
${
encodeURIComponent
(
p
)}
&ref=
${
encodeURIComponent
(
r
||
""
)}
`
,
t
);
export
const
gitlabGetBranches
=
(
token
,
repoId
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
repoId
}
/branches`
,
token
);
export
const
gitlabGetBranches
=
(
t
,
id
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
id
}
/branches`
,
t
);
export
const
gitlabCommit
=
(
token
,
repoId
,
data
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
repoId
}
/commit`
,
token
,
data
);
export
const
gitlabCreateBranch
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/branches`
,
t
,
d
);
export
const
gitlabCommitSingle
=
(
token
,
repoId
,
data
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
repoId
}
/commit-single`
,
token
,
data
);
export
const
gitlabCommit
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/commit`
,
t
,
d
);
export
const
gitlabCreateBranch
=
(
token
,
repoId
,
data
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
repoId
}
/branches`
,
token
,
data
);
export
const
gitlabCommitSingle
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/commit-single`
,
t
,
d
);
export
const
gitlabCreateMR
=
(
token
,
repoId
,
data
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
repoId
}
/merge-request`
,
token
,
data
);
export
const
gitlabCreateMR
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/merge-request`
,
t
,
d
);
export
const
gitlabAnalyzeRepo
=
(
token
,
repoId
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
repoId
}
/analyze`
,
token
);
export
const
gitlabReanalyzeRepo
=
(
t
,
id
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/analyze`
,
t
);
export
const
gitlabGetRepoMap
=
(
token
,
repoId
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
repoId
}
/map`
,
token
);
export
const
gitlabGetRepoMap
=
(
t
,
id
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
id
}
/map`
,
t
);
export
const
gitlabListActions
=
(
token
,
status
)
=>
request
(
"GET"
,
`/gitlab/actions?status=
${
status
||
"pending"
}
`
,
token
);
export
const
gitlabListActions
=
(
t
,
s
)
=>
request
(
"GET"
,
`/gitlab/actions?status=
${
s
||
"pending"
}
`
,
t
);
export
const
gitlabCreateAction
=
(
token
,
data
)
=>
request
(
"POST"
,
"/gitlab/actions"
,
token
,
data
);
export
const
gitlabCreateAction
=
(
t
,
d
)
=>
request
(
"POST"
,
"/gitlab/actions"
,
t
,
d
);
export
const
gitlabApproveAction
=
(
token
,
actionId
)
=>
request
(
"POST"
,
`/gitlab/actions/
${
actionId
}
/approve`
,
token
);
export
const
gitlabApproveAction
=
(
t
,
id
)
=>
request
(
"POST"
,
`/gitlab/actions/
${
id
}
/approve`
,
t
);
export
const
gitlabRejectAction
=
(
token
,
actionId
)
=>
request
(
"POST"
,
`/gitlab/actions/
${
actionId
}
/reject`
,
token
);
export
const
gitlabRejectAction
=
(
t
,
id
)
=>
request
(
"POST"
,
`/gitlab/actions/
${
id
}
/reject`
,
t
);
\ No newline at end of file
\ No newline at end of file
frontend/src/pages/AdminPage.jsx
View file @
49192084
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
,
valu
e
)
{
async
function
handleToggleActive
(
userId
,
currentActiv
e
)
{
setPermsData
(
prev
=>
({
...
prev
,
[
key
]:
value
}));
try
{
await
adminUpdateUser
(
token
,
userId
,
{
is_active
:
!
currentActive
});
flash
(
"User updated"
);
load
();
}
catch
(
e
)
{
setError
(
e
.
message
);
}
}
}
function
formatNum
(
n
)
{
async
function
openPermissions
(
userId
)
{
if
(
n
>=
1
_000_000
)
return
(
n
/
1
_000_000
).
toFixed
(
1
)
+
"M"
;
try
{
if
(
n
>=
1
_000
)
return
(
n
/
1
_000
).
toFixed
(
0
)
+
"K"
;
const
[
p
,
d
]
=
await
Promise
.
all
([
adminGetUserPermissions
(
token
,
userId
),
adminGetPermissionDefaults
(
token
)]);
return
String
(
n
);
setPermUserId
(
userId
);
setPermData
(
p
);
setDefaults
(
d
);
}
catch
(
e
)
{
setError
(
e
.
message
);
}
}
}
if
(
state
.
user
?.
role
!==
"superadmin"
)
{
async
function
savePermissions
(
)
{
return
<
div
className=
"h-full flex items-center justify-center"
><
p
className=
"text-anton-danger text-lg"
>
⛔ Access Denied
</
p
></
div
>;
try
{
await
adminUpdateUserPermissions
(
token
,
permUserId
,
permData
);
setPermUserId
(
null
);
flash
(
"Permissions saved"
);
}
catch
(
e
)
{
setError
(
e
.
message
);
}
}
}
const
TABS
=
[
{
id
:
"stats"
,
label
:
"Dashboard"
,
icon
:
BarChart3
},
{
id
:
"users"
,
label
:
"Users"
,
icon
:
Users
},
{
id
:
"settings"
,
label
:
"Settings"
,
icon
:
Settings2
},
];
return
(
return
(
<
div
className=
"h-full overflow-y-auto bg-anton-bg p-4 sm:p-6"
>
<
div
className=
"flex-1 flex flex-col min-h-0 bg-anton-bg"
>
<
div
className=
"max-w-6xl mx-auto space-y-6 animate-fade-in"
>
{
/* Header */
}
{
/* Header */
}
<
div
className=
"border-b border-anton-border bg-anton-surface px-6 py-4"
>
<
div
className=
"flex items-center gap-4"
>
<
div
className=
"flex items-center justify-between"
>
<
button
onClick=
{
()
=>
navigate
(
"/"
)
}
className=
"p-2 rounded-lg bg-anton-surface border border-anton-border hover:border-anton-accent transition"
><
ArrowLeft
size=
{
20
}
/></
button
>
<
div
>
<
div
>
<
h1
className=
"text-
2xl font-bold text-white flex items-center gap-2"
><
Shield
size=
{
24
}
className=
"text-anton-accent"
/>
Admin Panel
</
h1
>
<
h1
className=
"text-
xl font-bold text-white flex items-center gap-2"
><
Shield
size=
{
20
}
className=
"text-anton-accent"
/>
Admin Dashboard
</
h1
>
<
p
className=
"text-
anton-muted text-sm"
>
Users, permissions
&
cost contro
l
</
p
>
<
p
className=
"text-
xs text-anton-muted mt-0.5"
>
Superadmin Control Pane
l
</
p
>
</
div
>
</
div
>
<
button
onClick=
{
load
}
className=
"text-anton-muted hover:text-white transition p-2 rounded-lg hover:bg-anton-card"
><
RefreshCw
size=
{
16
}
/></
button
>
</
div
>
<
div
className=
"flex gap-1 mt-4"
>
{
TABS
.
map
((
t
)
=>
(
<
button
key=
{
t
.
id
}
onClick=
{
()
=>
setTab
(
t
.
id
)
}
className=
{
`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition ${tab === t.id ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white hover:bg-anton-card"}`
}
>
<
t
.
icon
size=
{
14
}
/>
{
t
.
label
}
</
button
>
))
}
</
div
>
</
div
>
</
div
>
{
/* Notifications */
}
{
error
&&
<
div
className=
"mx-6 mt-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center justify-between"
>
{
error
}
<
button
onClick=
{
()
=>
setError
(
""
)
}
><
X
size=
{
14
}
/></
button
></
div
>
}
{
success
&&
<
div
className=
"mx-6 mt-3 p-3 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400 text-sm"
>
{
success
}
</
div
>
}
{
error
&&
(<
div
className=
"bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3"
>
{
error
}
<
button
onClick=
{
()
=>
setError
(
""
)
}
className=
"ml-2 text-red-300 hover:text-white"
>
✕
</
button
></
div
>)
}
{
/* Content */
}
<
div
className=
"flex-1 overflow-y-auto p-6 space-y-6"
>
{
loading
&&
!
stats
&&
<
div
className=
"text-center text-anton-muted py-12"
>
Loading...
</
div
>
}
{
/*
Stats
*/
}
{
/*
═══ STATS TAB ═══
*/
}
{
stats
&&
(
{
tab
===
"stats"
&&
stats
&&
(
<
div
className=
"grid grid-cols-2 md:grid-cols-
4
gap-4"
>
<
div
className=
"grid grid-cols-2 md:grid-cols-
3 lg:grid-cols-6
gap-4"
>
{
[
{
[
{
label
:
"Users"
,
value
:
stats
.
total_users
,
icon
:
Users
,
color
:
"text-blue-400"
},
{
label
:
"Users"
,
value
:
stats
.
total_users
,
icon
:
Users
,
color
:
"text-blue-400"
},
{
label
:
"Chats"
,
value
:
stats
.
total_chats
,
icon
:
MessageSquare
,
color
:
"text-green-400"
},
{
label
:
"Active"
,
value
:
stats
.
active_users
,
icon
:
Check
,
color
:
"text-green-400"
},
{
label
:
"Messages"
,
value
:
formatNum
(
stats
.
total_messages
),
icon
:
Zap
,
color
:
"text-anton-accent"
},
{
label
:
"Chats"
,
value
:
stats
.
total_chats
,
icon
:
MessageSquare
,
color
:
"text-purple-400"
},
{
label
:
"Tokens Used"
,
value
:
formatNum
(
stats
.
total_tokens_used
),
icon
:
Database
,
color
:
"text-purple-400"
},
{
label
:
"Messages"
,
value
:
stats
.
total_messages
,
icon
:
MessageSquare
,
color
:
"text-cyan-400"
},
].
map
((
s
)
=>
(
{
label
:
"Tokens Used"
,
value
:
(
stats
.
total_tokens_used
/
1000000
).
toFixed
(
1
)
+
"M"
,
icon
:
Brain
,
color
:
"text-yellow-400"
},
<
div
key=
{
s
.
label
}
className=
"bg-anton-surface border border-anton-border rounded-xl p-4"
>
{
label
:
"Knowledge"
,
value
:
stats
.
total_knowledge_bases
,
icon
:
Database
,
color
:
"text-orange-400"
},
<
div
className=
"flex items-center gap-2 mb-1"
><
s
.
icon
size=
{
16
}
className=
{
s
.
color
}
/><
span
className=
"text-anton-muted text-sm"
>
{
s
.
label
}
</
span
></
div
>
].
map
((
s
,
i
)
=>
(
<
p
className=
"text-2xl font-bold text-white"
>
{
s
.
value
}
</
p
>
<
div
key=
{
i
}
className=
"bg-anton-card border border-anton-border rounded-xl p-4"
>
<
div
className=
"flex items-center gap-2 mb-2"
><
s
.
icon
size=
{
14
}
className=
{
s
.
color
}
/><
span
className=
"text-xs text-anton-muted"
>
{
s
.
label
}
</
span
></
div
>
<
div
className=
"text-2xl font-bold text-white"
>
{
typeof
s
.
value
===
"number"
?
s
.
value
.
toLocaleString
()
:
s
.
value
}
</
div
>
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
)
}
)
}
{
/* User Management */
}
{
/* ═══ USERS TAB ═══ */
}
<
div
className=
"bg-anton-surface border border-anton-border rounded-xl overflow-hidden"
>
{
tab
===
"users"
&&
(
<
div
className=
"px-5 py-4 border-b border-anton-border flex items-center justify-between flex-wrap gap-2"
>
<>
<
h2
className=
"text-lg font-semibold text-white"
>
Users
</
h2
>
<
div
className=
"flex items-center justify-between"
>
<
div
className=
"flex items-center gap-2"
>
<
h2
className=
"text-lg font-semibold text-white"
>
Users (
{
users
.
length
}
)
</
h2
>
<
button
onClick=
{
()
=>
openPerms
({
id
:
"__defaults__"
,
username
:
"Default Template"
})
}
className=
"flex items-center gap-1.5 px-3 py-1.5 bg-purple-500/20 text-purple-400 border border-purple-500/30 rounded-lg text-sm font-medium hover:bg-purple-500/30 transition"
title=
"Edit default permissions for new users"
>
<
button
onClick=
{
()
=>
setShowCreate
(
!
showCreate
)
}
className=
"flex items-center gap-1.5 px-4 py-2 bg-anton-accent text-white rounded-lg text-sm hover:opacity-90 transition"
>
<
Lock
size=
{
14
}
/>
Defaults
<
Plus
size=
{
14
}
/>
Create User
</
button
>
<
button
onClick=
{
handleApplyDefaults
}
disabled=
{
applyingDefaults
}
className=
"flex items-center gap-1.5 px-3 py-1.5 bg-anton-card border border-anton-border text-anton-muted rounded-lg text-sm hover:text-white transition disabled:opacity-50"
>
{
applyingDefaults
?
<
Loader2
size=
{
14
}
className=
"animate-spin"
/>
:
<
Copy
size=
{
14
}
/>
}
Apply to All
</
button
>
<
button
onClick=
{
()
=>
setShowCreate
(
!
showCreate
)
}
className=
"flex items-center gap-1.5 px-3 py-1.5 bg-anton-accent text-white rounded-lg text-sm font-medium hover:opacity-90 transition"
>
{
showCreate
?
<
X
size=
{
14
}
/>
:
<
UserPlus
size=
{
14
}
/>
}
{
showCreate
?
"Cancel"
:
"New User"
}
</
button
>
</
button
>
</
div
>
</
div
>
</
div
>
{
showCreate
&&
(
{
showCreate
&&
(
<
form
onSubmit=
{
handleCreate
}
className=
"px-5 py-4 border-b border-anton-border bg-anton-card grid grid-cols-1 md:grid-cols-3 gap-3"
>
<
form
onSubmit=
{
handleCreateUser
}
className=
"bg-anton-card border border-anton-border rounded-xl p-4 space-y-3 animate-fade-in"
>
<
input
placeholder=
"Username"
required
value=
{
newUser
.
username
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
username
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
div
className=
"grid grid-cols-2 gap-3"
>
<
input
placeholder=
"Email"
type=
"email"
required
value=
{
newUser
.
email
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
email
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
input
value=
{
newUser
.
username
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
username
:
e
.
target
.
value
})
}
placeholder=
"Username"
required
<
input
placeholder=
"Password"
required
value=
{
newUser
.
password
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
password
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
select
value=
{
newUser
.
role
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
role
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
>
<
input
value=
{
newUser
.
email
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
email
:
e
.
target
.
value
})
}
placeholder=
"Email"
required
type=
"email"
<
option
value=
"user"
>
User
</
option
><
option
value=
"admin"
>
Admin
</
option
><
option
value=
"superadmin"
>
Superadmin
</
option
>
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
</
select
>
<
input
value=
{
newUser
.
password
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
password
:
e
.
target
.
value
})
}
placeholder=
"Password"
required
type=
"password"
<
input
placeholder=
"Monthly quota"
type=
"number"
value=
{
newUser
.
quota_tokens_monthly
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
quota_tokens_monthly
:
Number
(
e
.
target
.
value
)
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
button
type=
"submit"
className=
"bg-anton-success text-white rounded-lg px-3 py-2 text-sm font-medium hover:opacity-90"
>
Create
</
button
>
<
select
value=
{
newUser
.
role
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
role
:
e
.
target
.
value
})
}
</
form
>
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
>
)
}
<
option
value=
"user"
>
User
</
option
><
option
value=
"admin"
>
Admin
</
option
>
</
select
>
<
div
className=
"overflow-x-auto"
>
</
div
>
<
table
className=
"w-full text-sm"
>
<
div
className=
"flex gap-2"
>
<
thead
><
tr
className=
"text-left text-anton-muted border-b border-anton-border"
>
<
button
type=
"submit"
className=
"px-4 py-2 bg-anton-accent text-white rounded-lg text-sm"
>
Create
</
button
>
<
th
className=
"px-5 py-3"
>
User
</
th
><
th
className=
"px-5 py-3"
>
Role
</
th
><
th
className=
"px-5 py-3"
>
Quota
</
th
><
th
className=
"px-5 py-3"
>
Used
</
th
><
th
className=
"px-5 py-3"
>
Chats
</
th
><
th
className=
"px-5 py-3"
>
Status
</
th
><
th
className=
"px-5 py-3"
>
Actions
</
th
>
<
button
type=
"button"
onClick=
{
()
=>
setShowCreate
(
false
)
}
className=
"px-4 py-2 bg-anton-card border border-anton-border text-white rounded-lg text-sm"
>
Cancel
</
button
>
</
tr
></
thead
>
<
tbody
>
{
users
.
map
((
u
)
=>
(
<
tr
key=
{
u
.
id
}
className=
"border-b border-anton-border/50 hover:bg-anton-card/50 transition"
>
<
td
className=
"px-5 py-3"
><
div
className=
"text-white font-medium"
>
{
u
.
username
}
</
div
><
div
className=
"text-anton-muted text-xs"
>
{
u
.
email
}
</
div
></
td
>
<
td
className=
"px-5 py-3"
>
{
editId
===
u
.
id
?
(
<
select
value=
{
editData
.
role
??
u
.
role
}
onChange=
{
(
e
)
=>
setEditData
({
...
editData
,
role
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs"
>
<
option
value=
"user"
>
user
</
option
><
option
value=
"admin"
>
admin
</
option
><
option
value=
"superadmin"
>
superadmin
</
option
>
</
select
>
)
:
(
<
span
className=
{
`px-2 py-0.5 rounded-full text-xs font-medium ${u.role === "superadmin" ? "bg-anton-accent/20 text-anton-accent" : u.role === "admin" ? "bg-blue-500/20 text-blue-400" : "bg-anton-border text-anton-muted"}`
}
>
{
u
.
role
}
</
span
>
)
}
</
td
>
<
td
className=
"px-5 py-3 text-anton-muted"
>
{
editId
===
u
.
id
?
<
input
type=
"number"
value=
{
editData
.
quota_tokens_monthly
??
u
.
quota_tokens_monthly
}
onChange=
{
(
e
)
=>
setEditData
({
...
editData
,
quota_tokens_monthly
:
Number
(
e
.
target
.
value
)
})
}
className=
"bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs w-28"
/>
:
formatNum
(
u
.
quota_tokens_monthly
)
}
</
td
>
<
td
className=
"px-5 py-3 text-anton-muted"
>
{
formatNum
(
u
.
tokens_used_this_month
)
}
</
td
>
<
td
className=
"px-5 py-3 text-anton-muted"
>
{
u
.
chat_count
}
</
td
>
<
td
className=
"px-5 py-3"
><
span
className=
{
`w-2 h-2 inline-block rounded-full mr-1 ${u.is_active ? "bg-anton-success" : "bg-anton-danger"}`
}
/><
span
className=
"text-xs text-anton-muted"
>
{
u
.
is_active
?
"Active"
:
"Off"
}
</
span
></
td
>
<
td
className=
"px-5 py-3"
>
<
div
className=
"flex items-center gap-1"
>
{
editId
===
u
.
id
?
(
<><
button
onClick=
{
()
=>
handleSaveEdit
(
u
.
id
)
}
className=
"p-1 rounded hover:bg-anton-success/20 text-anton-success"
><
Save
size=
{
14
}
/></
button
><
button
onClick=
{
()
=>
setEditId
(
null
)
}
className=
"p-1 rounded hover:bg-anton-border text-anton-muted"
><
X
size=
{
14
}
/></
button
></>
)
:
(
<>
{
u
.
role
!==
"superadmin"
&&
(
<
button
onClick=
{
()
=>
openPerms
(
u
)
}
className=
"p-1 rounded hover:bg-purple-500/20 text-purple-400"
title=
"Permissions"
><
Settings2
size=
{
14
}
/></
button
>
)
}
<
button
onClick=
{
()
=>
{
setEditId
(
u
.
id
);
setEditData
({});
}
}
className=
"p-1 rounded hover:bg-anton-accent/20 text-anton-accent text-xs"
>
Edit
</
button
>
<
button
onClick=
{
()
=>
adminUpdateUser
(
state
.
token
,
u
.
id
,
{
is_active
:
!
u
.
is_active
}).
then
(
load
)
}
className=
"p-1 rounded hover:bg-anton-border text-anton-muted"
title=
{
u
.
is_active
?
"Disable"
:
"Enable"
}
>
{
u
.
is_active
?
<
ShieldOff
size=
{
14
}
/>
:
<
Shield
size=
{
14
}
/>
}
</
button
>
{
u
.
role
!==
"superadmin"
&&
<
button
onClick=
{
()
=>
handleDelete
(
u
.
id
,
u
.
username
)
}
className=
"p-1 rounded hover:bg-red-500/20 text-anton-danger"
><
Trash2
size=
{
14
}
/></
button
>
}
</>
)
}
</
div
>
</
td
>
</
tr
>
))
}
</
tbody
>
</
table
>
</
div
>
</
div
>
</
div
>
{
/* ═══ PERMISSIONS MODAL ═══ */
}
{
permsUser
&&
(
<
div
className=
"fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-start justify-center overflow-y-auto p-4 animate-fade-in"
onClick=
{
()
=>
{
setPermsUser
(
null
);
setPermsData
(
null
);
}
}
>
<
div
className=
"bg-anton-surface border border-anton-border rounded-2xl w-full max-w-2xl my-8 shadow-2xl"
onClick=
{
e
=>
e
.
stopPropagation
()
}
>
{
/* Modal Header */
}
<
div
className=
"px-6 py-4 border-b border-anton-border flex items-center justify-between"
>
<
div
>
<
h2
className=
"text-lg font-bold text-white flex items-center gap-2"
>
<
Settings2
size=
{
20
}
className=
"text-purple-400"
/>
{
permsUser
.
id
===
"__defaults__"
?
"Default Permissions Template"
:
`Permissions: @${permsUser.username}`
}
</
h2
>
<
p
className=
"text-xs text-anton-muted mt-0.5"
>
{
permsUser
.
id
===
"__defaults__"
?
"Applied to new users on creation"
:
`Role: ${permsUser.role}`
}
</
p
>
</
div
>
<
button
onClick=
{
()
=>
{
setPermsUser
(
null
);
setPermsData
(
null
);
}
}
className=
"p-2 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"
><
X
size=
{
18
}
/></
button
>
</
div
>
{
permsLoading
?
(
<
div
className=
"flex items-center justify-center py-20"
><
Loader2
size=
{
24
}
className=
"text-anton-accent animate-spin"
/></
div
>
)
:
permsData
?
(
<
div
className=
"p-6 space-y-6 max-h-[70vh] overflow-y-auto"
>
{
/* Feature Access */
}
<
div
>
<
h3
className=
"text-sm font-semibold text-white mb-3 flex items-center gap-2"
><
Lock
size=
{
14
}
className=
"text-anton-accent"
/>
Feature Access
</
h3
>
<
div
className=
"space-y-2"
>
{
FEATURE_DEFS
.
map
(
f
=>
{
const
Icon
=
f
.
icon
;
return
(
<
div
key=
{
f
.
key
}
className=
"flex items-center justify-between bg-anton-card rounded-lg px-4 py-3 border border-anton-border"
>
<
div
className=
"flex items-center gap-3"
>
<
Icon
size=
{
16
}
className=
{
f
.
color
}
/>
<
div
>
<
p
className=
"text-sm text-white font-medium"
>
{
f
.
label
}
</
p
>
<
p
className=
"text-[10px] text-anton-muted"
>
{
f
.
desc
}
</
p
>
</
div
>
</
div
>
<
Toggle
value=
{
!!
permsData
[
f
.
key
]
}
onChange=
{
v
=>
updatePerm
(
f
.
key
,
v
)
}
/>
</
div
>
);
})
}
</
div
>
</
div
>
</
div
>
</
form
>
)
}
{
/* Model Access */
}
<
div
className=
"space-y-2"
>
<
div
>
{
users
.
map
((
u
)
=>
(
<
h3
className=
"text-sm font-semibold text-white mb-3 flex items-center gap-2"
><
Cpu
size=
{
14
}
className=
"text-cyan-400"
/>
Model Access
</
h3
>
<
div
key=
{
u
.
id
}
className=
"bg-anton-card border border-anton-border rounded-xl p-4 flex items-center gap-4"
>
<
div
className=
"space-y-2"
>
<
div
className=
"flex-1 min-w-0"
>
<
label
className=
"flex items-center gap-3 bg-anton-card rounded-lg px-4 py-3 border border-anton-border cursor-pointer"
>
<
div
className=
"flex items-center gap-2"
>
<
input
type=
"checkbox"
checked=
{
permsData
.
allowed_models
===
"all"
}
onChange=
{
e
=>
updatePerm
(
"allowed_models"
,
e
.
target
.
checked
?
"all"
:
MODELS
.
map
(
m
=>
m
.
id
).
join
(
","
))
}
<
span
className=
"text-white font-medium text-sm"
>
{
u
.
username
}
</
span
>
className=
"w-4 h-4 rounded accent-anton-accent"
/>
<
span
className=
{
`text-[10px] px-1.5 py-0.5 rounded ${u.role === "superadmin" ? "bg-red-500/20 text-red-400" : u.role === "admin" ? "bg-yellow-500/20 text-yellow-400" : "bg-blue-500/20 text-blue-400"}`
}
>
{
u
.
role
}
</
span
>
<
div
><
p
className=
"text-sm text-white font-medium"
>
All Models
</
p
><
p
className=
"text-[10px] text-anton-muted"
>
Access to every available model
</
p
></
div
>
{
!
u
.
is_active
&&
<
span
className=
"text-[10px] px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400"
>
disabled
</
span
>
}
</
label
>
</
div
>
{
permsData
.
allowed_models
!==
"all"
&&
(
<
div
className=
"text-xs text-anton-muted mt-0.5"
>
{
u
.
email
}
•
{
u
.
chat_count
}
chats •
{
((
u
.
tokens_used_this_month
||
0
)
/
1000000
).
toFixed
(
2
)
}
M /
{
((
u
.
quota_tokens_monthly
||
0
)
/
1000000
).
toFixed
(
0
)
}
M tokens
</
div
>
<
div
className=
"pl-4 space-y-1.5"
>
{
MODELS
.
map
(
m
=>
{
const
list
=
(
permsData
.
allowed_models
||
""
).
split
(
","
).
map
(
s
=>
s
.
trim
()).
filter
(
Boolean
);
const
checked
=
list
.
includes
(
m
.
id
);
return
(
<
label
key=
{
m
.
id
}
className=
"flex items-center gap-3 bg-anton-bg rounded-lg px-3 py-2 border border-anton-border/50 cursor-pointer"
>
<
input
type=
"checkbox"
checked=
{
checked
}
onChange=
{
e
=>
{
let
next
=
checked
?
list
.
filter
(
x
=>
x
!==
m
.
id
)
:
[...
list
,
m
.
id
];
if
(
!
next
.
length
)
next
=
[
m
.
id
];
// Can't have zero models
updatePerm
(
"allowed_models"
,
next
.
join
(
","
));
}
}
className=
"w-4 h-4 rounded accent-anton-accent"
/>
<
div
><
span
className=
"text-sm text-white"
>
{
m
.
label
}
</
span
><
span
className=
"text-[10px] text-anton-muted ml-2"
>
{
m
.
tier
}
</
span
></
div
>
</
label
>
);
})
}
</
div
>
)
}
</
div
>
</
div
>
{
u
.
role
!==
"superadmin"
&&
(
<
div
className=
"flex items-center gap-1"
>
<
button
onClick=
{
()
=>
handleToggleActive
(
u
.
id
,
u
.
is_active
)
}
title=
{
u
.
is_active
?
"Disable"
:
"Enable"
}
className=
"p-1.5 rounded-lg hover:bg-anton-bg transition text-anton-muted hover:text-white"
>
{
u
.
is_active
?
<
Unlock
size=
{
14
}
/>
:
<
Lock
size=
{
14
}
/>
}
</
button
>
<
button
onClick=
{
()
=>
openPermissions
(
u
.
id
)
}
title=
"Permissions"
className=
"p-1.5 rounded-lg hover:bg-anton-bg transition text-anton-muted hover:text-purple-400"
>
<
Shield
size=
{
14
}
/>
</
button
>
<
button
onClick=
{
()
=>
handleDeleteUser
(
u
.
id
,
u
.
username
)
}
title=
"Delete"
className=
"p-1.5 rounded-lg hover:bg-anton-bg transition text-anton-muted hover:text-red-400"
>
<
Trash2
size=
{
14
}
/>
</
button
>
</
div
>
)
}
</
div
>
</
div
>
))
}
</
div
>
{
/* Limits */
}
{
/* Permissions Modal */
}
<
div
>
{
permUserId
&&
permData
&&
(
<
h3
className=
"text-sm font-semibold text-white mb-3 flex items-center gap-2"
><
Gauge
size=
{
14
}
className=
"text-amber-400"
/>
Usage Limits
</
h3
>
<
div
className=
"fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4"
onClick=
{
()
=>
setPermUserId
(
null
)
}
>
<
div
className=
"grid grid-cols-1 sm:grid-cols-2 gap-3"
>
<
div
className=
"bg-anton-card border border-anton-border rounded-2xl p-6 w-full max-w-lg max-h-[80vh] overflow-y-auto"
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
{
LIMIT_DEFS
.
map
(
l
=>
(
<
h3
className=
"text-lg font-semibold text-white mb-4 flex items-center gap-2"
><
Shield
size=
{
18
}
className=
"text-purple-400"
/>
User Permissions
</
h3
>
<
div
key=
{
l
.
key
}
className=
"bg-anton-card rounded-lg px-4 py-3 border border-anton-border"
>
<
div
className=
"space-y-3"
>
<
div
className=
"flex justify-between items-center mb-1"
>
{
[
"can_use_web_search"
,
"can_use_ui_design"
,
"can_use_knowledge_base"
,
"can_use_gitlab"
,
"can_use_attachments"
,
"can_export_pptx"
,
"can_export_docx"
].
map
((
key
)
=>
(
<
label
className=
"text-xs text-anton-muted"
>
{
l
.
label
}
</
label
>
<
label
key=
{
key
}
className=
"flex items-center justify-between"
>
<
span
className=
"text-xs text-white font-mono"
>
{
(
permsData
[
l
.
key
]
||
0
)
===
0
&&
l
.
desc
?.
includes
(
"unlimited"
)
?
"∞"
:
(
permsData
[
l
.
key
]
||
0
).
toLocaleString
()
}
</
span
>
<
span
className=
"text-sm text-white"
>
{
key
.
replace
(
/^can_/
,
""
).
replace
(
/_/g
,
" "
).
replace
(
/
\b\w
/g
,
(
c
)
=>
c
.
toUpperCase
())
}
</
span
>
</
div
>
<
button
onClick=
{
()
=>
setPermData
({
...
permData
,
[
key
]:
!
permData
[
key
]
})
}
<
input
type=
"number"
min=
{
l
.
min
}
max=
{
l
.
max
}
step=
{
l
.
step
}
value=
{
permsData
[
l
.
key
]
||
0
}
className=
{
`transition ${permData[key] ? "text-green-400" : "text-anton-muted"}`
}
>
onChange=
{
e
=>
updatePerm
(
l
.
key
,
Math
.
min
(
Math
.
max
(
Number
(
e
.
target
.
value
)
||
0
,
l
.
min
),
l
.
max
))
}
{
permData
[
key
]
?
<
ToggleRight
size=
{
24
}
/>
:
<
ToggleLeft
size=
{
24
}
/>
}
className=
"w-full bg-anton-bg border border-anton-border rounded px-2.5 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-anton-accent"
</
button
>
/>
</
label
>
<
p
className=
"text-[9px] text-anton-muted mt-1"
>
{
l
.
desc
}
</
p
>
</
div
>
))
}
))
}
<
div
className=
"border-t border-anton-border pt-3 space-y-3"
>
<
div
><
label
className=
"text-xs text-anton-muted"
>
Allowed Models
</
label
>
<
input
value=
{
permData
.
allowed_models
||
""
}
onChange=
{
(
e
)
=>
setPermData
({
...
permData
,
allowed_models
:
e
.
target
.
value
})
}
className=
"w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm mt-1 focus:outline-none focus:border-anton-accent"
/></
div
>
{
[
"max_tokens_cap"
,
"max_reasoning_budget"
,
"max_chats"
,
"max_messages_per_day"
,
"max_knowledge_bases"
,
"max_documents_per_kb"
,
"max_attachment_size_mb"
,
"max_attachments_per_message"
].
map
((
key
)
=>
(
<
div
key=
{
key
}
><
label
className=
"text-xs text-anton-muted"
>
{
key
.
replace
(
/_/g
,
" "
).
replace
(
/
\b\w
/g
,
(
c
)
=>
c
.
toUpperCase
())
}
{
key
.
includes
(
"max_"
)
&&
permData
[
key
]
===
0
?
"(unlimited)"
:
""
}
</
label
>
<
input
type=
"number"
value=
{
permData
[
key
]
||
0
}
onChange=
{
(
e
)
=>
setPermData
({
...
permData
,
[
key
]:
parseInt
(
e
.
target
.
value
)
||
0
})
}
className=
"w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm mt-1 focus:outline-none focus:border-anton-accent"
/></
div
>
))
}
</
div
>
</
div
>
<
div
className=
"flex gap-2 mt-4"
>
<
button
onClick=
{
savePermissions
}
className=
"flex items-center gap-1.5 px-4 py-2 bg-anton-accent text-white rounded-lg text-sm"
><
Save
size=
{
14
}
/>
Save
</
button
>
<
button
onClick=
{
()
=>
setPermUserId
(
null
)
}
className=
"px-4 py-2 bg-anton-card border border-anton-border text-white rounded-lg text-sm"
>
Cancel
</
button
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
)
:
null
}
)
}
</>
)
}
{
/* Modal Footer */
}
{
/* ═══ SETTINGS TAB ═══ */
}
{
permsData
&&
(
{
tab
===
"settings"
&&
(
<
div
className=
"px-6 py-4 border-t border-anton-border flex items-center justify-between"
>
<
div
className=
"space-y-6"
>
<
button
onClick=
{
()
=>
{
setPermsUser
(
null
);
setPermsData
(
null
);
}
}
className=
"px-4 py-2 rounded-lg border border-anton-border text-anton-muted hover:text-white transition text-sm"
>
Cancel
</
button
>
{
/* Registration Toggle */
}
<
button
onClick=
{
savePerms
}
disabled=
{
permsSaving
}
className=
"flex items-center gap-2 px-5 py-2 bg-anton-accent text-white rounded-lg text-sm font-semibold hover:opacity-80 transition disabled:opacity-50"
>
<
div
className=
"bg-anton-card border border-anton-border rounded-xl p-6"
>
{
permsSaving
?
<
Loader2
size=
{
14
}
className=
"animate-spin"
/>
:
<
Save
size=
{
14
}
/>
}
Save Permissions
<
h3
className=
"text-lg font-semibold text-white mb-1 flex items-center gap-2"
><
UserPlus
size=
{
18
}
className=
"text-blue-400"
/>
User Registration
</
h3
>
</
button
>
<
p
className=
"text-xs text-anton-muted mb-4"
>
Control whether new users can register accounts themselves.
</
p
>
{
appSettings
&&
(
<
div
className=
"flex items-center justify-between bg-anton-bg border border-anton-border rounded-xl p-4"
>
<
div
>
<
div
className=
"text-sm text-white font-medium"
>
Registration is
{
appSettings
.
registration_enabled
?
<
span
className=
"text-green-400"
>
ENABLED
</
span
>
:
<
span
className=
"text-red-400"
>
DISABLED
</
span
>
}
</
div
>
<
div
className=
"text-xs text-anton-muted mt-0.5"
>
{
appSettings
.
registration_enabled
?
"Anyone can create a new account from the login page."
:
"Only superadmins can create new user accounts."
}
</
div
>
</
div
>
<
button
onClick=
{
toggleRegistration
}
className=
{
`transition-colors ${appSettings.registration_enabled ? "text-green-400 hover:text-green-300" : "text-anton-muted hover:text-white"}`
}
>
{
appSettings
.
registration_enabled
?
<
ToggleRight
size=
{
36
}
/>
:
<
ToggleLeft
size=
{
36
}
/>
}
</
button
>
</
div
>
)
}
</
div
>
{
/* Recent Chats */
}
<
div
className=
"bg-anton-card border border-anton-border rounded-xl p-6"
>
<
h3
className=
"text-lg font-semibold text-white mb-4 flex items-center gap-2"
><
MessageSquare
size=
{
18
}
className=
"text-purple-400"
/>
Recent Chats
</
h3
>
<
div
className=
"space-y-2 max-h-80 overflow-y-auto"
>
{
chats
.
slice
(
0
,
50
).
map
((
c
)
=>
(
<
div
key=
{
c
.
id
}
className=
"flex items-center justify-between bg-anton-bg border border-anton-border rounded-lg p-3"
>
<
div
className=
"min-w-0"
>
<
div
className=
"text-sm text-white truncate"
>
{
c
.
title
}
</
div
>
<
div
className=
"text-xs text-anton-muted"
>
{
c
.
username
}
•
{
c
.
message_count
}
msgs
</
div
>
</
div
>
<
span
className=
"text-[10px] text-anton-muted shrink-0"
>
{
new
Date
(
c
.
updated_at
).
toLocaleDateString
()
}
</
span
>
</
div
>
))
}
{
chats
.
length
===
0
&&
<
div
className=
"text-center text-anton-muted text-sm py-4"
>
No chats yet
</
div
>
}
</
div
>
</
div
>
)
}
</
div
>
</
div
>
</
div
>
</
div
>
)
}
)
}
</
div
>
</
div
>
</
div
>
);
);
}
}
\ No newline at end of file
frontend/src/pages/LoginPage.jsx
View file @
49192084
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
[
is
Register
,
setIsRegister
]
=
useState
(
fals
e
);
const
[
is
Login
,
setIsLogin
]
=
useState
(
tru
e
);
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
[
showP
w
,
setShowPw
]
=
useState
(
false
);
const
[
showP
assword
,
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-
2
xl font-bold text-white"
>
Son of Anton
</
h1
>
<
h1
className=
"text-
3
xl font-bold text-white"
>
Son of Anton
</
h1
>
<
p
className=
"text-anton-muted
text-sm mt-1
"
>
Avatar of All Elements of Code
</
p
>
<
p
className=
"text-anton-muted
mt-1 text-sm
"
>
Avatar of All Elements of Code
</
p
>
</
div
>
</
div
>
{
/* Form */
}
<
div
className=
"bg-anton-card border border-anton-border rounded-2xl p-6 shadow-xl"
>
<
form
onSubmit=
{
handleSubmit
}
className=
"space-y-4"
>
{
!
checkingRegistration
&&
registrationEnabled
&&
(
<
div
>
<
div
className=
"flex mb-6 bg-anton-bg rounded-xl p-1"
>
<
label
className=
"text-xs text-anton-muted mb-1.5 block"
>
Username
</
label
>
<
button
onClick=
{
()
=>
{
setIsLogin
(
true
);
setError
(
""
);
}
}
<
input
className=
{
`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition ${isLogin ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"}`
}
>
type=
"text"
Sign In
value=
{
username
}
</
button
>
onChange=
{
(
e
)
=>
setUsername
(
e
.
target
.
value
)
}
<
button
onClick=
{
()
=>
{
setIsLogin
(
false
);
setError
(
""
);
}
}
className=
"w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 text-white focus:outline-none focus:border-anton-accent transition"
className=
{
`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition ${!isLogin ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"}`
}
>
placeholder=
"Enter username"
Register
required
autoComplete=
"username"
autoCapitalize=
"off"
/>
</
div
>
{
isRegister
&&
(
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1.5 block"
>
Email
</
label
>
<
input
type=
"email"
value=
{
email
}
onChange=
{
(
e
)
=>
setEmail
(
e
.
target
.
value
)
}
className=
"w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 text-white focus:outline-none focus:border-anton-accent transition"
placeholder=
"your@email.com"
required
autoComplete=
"email"
/>
</
div
>
)
}
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1.5 block"
>
Password
</
label
>
<
div
className=
"relative"
>
<
input
type=
{
showPw
?
"text"
:
"password"
}
value=
{
password
}
onChange=
{
(
e
)
=>
setPassword
(
e
.
target
.
value
)
}
className=
"w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 pr-12 text-white focus:outline-none focus:border-anton-accent transition"
placeholder=
"••••••••"
required
autoComplete=
{
isRegister
?
"new-password"
:
"current-password"
}
/>
<
button
type=
"button"
onClick=
{
()
=>
setShowPw
(
!
showPw
)
}
className=
"absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition p-1"
>
{
showPw
?
<
EyeOff
size=
{
18
}
/>
:
<
Eye
size=
{
18
}
/>
}
</
button
>
</
button
>
</
div
>
</
div
>
</
div
>
)
}
{
error
&&
(
{
error
&&
(
<
div
className=
"bg-anton-danger/10 border border-anton-danger/30 text-anton-danger text-sm rounded-lg px-3 py-2.5"
>
<
div
className=
"mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-2 text-red-400 text-sm"
>
<
AlertCircle
size=
{
16
}
className=
"shrink-0"
/>
{
error
}
{
error
}
</
div
>
</
div
>
)
}
)
}
<
button
<
form
onSubmit=
{
handleSubmit
}
className=
"space-y-4"
>
type=
"submit"
<
div
>
disabled=
{
loading
}
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Username
</
label
>
className=
"w-full py-3.5 bg-anton-accent text-white rounded-xl font-semibold hover:opacity-90 transition disabled:opacity-50 active:scale-[0.98] flex items-center justify-center gap-2"
<
input
type=
"text"
value=
{
username
}
onChange=
{
(
e
)
=>
setUsername
(
e
.
target
.
value
)
}
>
placeholder=
"Enter username"
required
autoFocus
{
loading
&&
<
Loader2
size=
{
18
}
className=
"animate-spin"
/>
}
className=
"w-full bg-anton-bg border border-anton-border rounded-xl px-4 py-3 text-white text-sm focus:outline-none focus:border-anton-accent transition"
/>
{
isRegister
?
"Create Account"
:
"Sign In"
}
</
div
>
</
button
>
{
!
isLogin
&&
(
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Email
</
label
>
<
input
type=
"email"
value=
{
email
}
onChange=
{
(
e
)
=>
setEmail
(
e
.
target
.
value
)
}
placeholder=
"Enter email"
required
className=
"w-full bg-anton-bg border border-anton-border rounded-xl px-4 py-3 text-white text-sm focus:outline-none focus:border-anton-accent transition"
/>
</
div
>
)
}
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Password
</
label
>
<
div
className=
"relative"
>
<
input
type=
{
showPassword
?
"text"
:
"password"
}
value=
{
password
}
onChange=
{
(
e
)
=>
setPassword
(
e
.
target
.
value
)
}
placeholder=
"Enter password"
required
className=
"w-full bg-anton-bg border border-anton-border rounded-xl px-4 py-3 pr-10 text-white text-sm focus:outline-none focus:border-anton-accent transition"
/>
<
button
type=
"button"
onClick=
{
()
=>
setShowPassword
(
!
showPassword
)
}
className=
"absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition"
>
{
showPassword
?
<
EyeOff
size=
{
16
}
/>
:
<
Eye
size=
{
16
}
/>
}
</
button
>
</
div
>
</
div
>
<
button
type=
"submit"
disabled=
{
loading
}
className=
"w-full bg-anton-accent text-white rounded-xl py-3 font-medium hover:opacity-90 transition disabled:opacity-50 flex items-center justify-center gap-2"
>
{
loading
?
(
<
span
className=
"w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"
/>
)
:
isLogin
?
(
<><
LogIn
size=
{
18
}
/>
Sign In
</>
)
:
(
<><
UserPlus
size=
{
18
}
/>
Create Account
</>
)
}
</
button
>
</
form
>
{
!
checkingRegistration
&&
!
registrationEnabled
&&
(
<
div
className=
"mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg text-yellow-400 text-xs text-center"
>
Registration is currently disabled. Contact your administrator.
</
div
>
)
}
</
div
>
<
button
<
p
className=
"text-center text-anton-muted text-xs mt-6"
>
type=
"button"
Created by Mahmoud Aglan — AL-Arcade
onClick=
{
()
=>
{
setIsRegister
(
!
isRegister
);
setError
(
""
);
}
}
</
p
>
className=
"w-full text-center text-sm text-anton-muted hover:text-white transition py-2"
>
{
isRegister
?
"Already have an account? Sign in"
:
"Need an account? Register"
}
</
button
>
</
form
>
</
div
>
</
div
>
</
div
>
</
div
>
);
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment