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
705bdcba
Commit
705bdcba
authored
Apr 10, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 5 files via Son of Anton
parent
e3643210
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
720 additions
and
414 deletions
+720
-414
admin_routes.py
backend/routes/admin_routes.py
+251
-12
auth_routes.py
backend/routes/auth_routes.py
+21
-11
api.js
frontend/src/api.js
+163
-7
AdminPage.jsx
frontend/src/pages/AdminPage.jsx
+211
-297
LoginPage.jsx
frontend/src/pages/LoginPage.jsx
+74
-87
No files found.
backend/routes/admin_routes.py
View file @
705bdcba
"""
Superadmin routes: user management, stats, permissions, app settings — 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
,
AppSettings
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
class
AppSettingsBody
(
BaseModel
):
allow_registration
:
Optional
[
bool
]
=
None
# ═══════════════════════════════════════════════════
#
REGISTRATION TOGGLE (append to end of file)
#
Stats & Users
# ═══════════════════════════════════════════════════
from
backend.models
import
AppSettings
# add this import at top if not already there
@
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
)
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
@
router
.
get
(
"/registration"
)
def
get_registration_setting
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
# ═══════════════════════════════════════════════════
# APP SETTINGS (registration toggle etc.)
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/settings"
)
def
get_app_settings
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
settings
=
db
.
query
(
AppSettings
)
.
first
()
if
not
settings
:
settings
=
AppSettings
(
allow_registration
=
True
)
db
.
add
(
settings
)
db
.
commit
()
db
.
refresh
(
settings
)
return
{
"allow_registration"
:
settings
.
allow_registration
}
return
{
"allow_registration"
:
settings
.
allow_registration
,
}
@
router
.
put
(
"/
registration
"
)
def
set_registration_setting
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
@
router
.
put
(
"/
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
(
allow_registration
=
True
)
db
.
add
(
settings
)
db
.
commit
()
db
.
refresh
(
settings
)
# Toggle
settings
.
allow_registration
=
not
settings
.
allow_registration
if
body
.
allow_registration
is
not
None
:
settings
.
allow_registration
=
body
.
allow_registration
db
.
commit
()
db
.
refresh
(
settings
)
return
{
"allow_registration"
:
settings
.
allow_registration
,
}
# ═══════════════════════════════════════════════════
# PERMISSIONS MANAGEMENT
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/models"
)
def
list_available_models
(
admin
:
User
=
Depends
(
require_superadmin
)):
return
AVAILABLE_MODELS
@
router
.
get
(
"/permissions/defaults"
)
def
get_defaults
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
return
get_default_permissions_template
(
db
)
@
router
.
put
(
"/permissions/defaults"
)
def
update_defaults
(
body
:
PermissionsBody
,
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
template
=
db
.
query
(
UserPermissions
)
.
filter
(
UserPermissions
.
user_id
==
"__defaults__"
)
.
first
()
if
not
template
:
template
=
UserPermissions
(
user_id
=
"__defaults__"
)
db
.
add
(
template
)
_apply_permissions_body
(
template
,
body
)
db
.
commit
()
return
{
"allow_registration"
:
settings
.
allow_registration
}
\ No newline at end of file
return
get_default_permissions_template
(
db
)
@
router
.
post
(
"/permissions/apply-defaults"
)
def
apply_defaults_to_all
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
"""Apply default permissions template to ALL non-superadmin users."""
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
()
return
{
"ok"
:
True
,
"users_updated"
:
count
}
@
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
backend/routes/auth_routes.py
View file @
705bdcba
"""
Authentication routes: register, login, profile — with permissions
+
registration toggle.
Authentication routes: register, login, profile — with permissions
and
registration toggle.
"""
from
pydantic
import
BaseModel
...
...
@@ -28,23 +28,22 @@ class LoginBody(BaseModel):
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
,
}
class
ProfileOut
(
BaseModel
):
pass
class
Config
(
BaseModel
):
pass
@
router
.
post
(
"/register"
)
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
:
settings
=
db
.
query
(
AppSettings
)
.
first
()
if
settings
and
not
settings
.
allow_registration
:
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Registration is currently disabled. Contact
an
administrator."
,
"Registration is currently disabled. Contact
your
administrator."
,
)
if
db
.
query
(
User
)
.
filter
(
...
...
@@ -63,6 +62,7 @@ def register(body: RegisterBody, db: Session = Depends(get_db)):
db
.
commit
()
db
.
refresh
(
user
)
# Auto-create permissions from defaults template
ensure_user_permissions
(
user
.
id
,
db
)
token
=
create_token
(
user
.
id
,
user
.
role
)
...
...
@@ -88,6 +88,16 @@ def me(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
return
_user_dict
(
user
,
perms
)
@
router
.
get
(
"/config"
)
def
get_public_config
(
db
:
Session
=
Depends
(
get_db
)):
"""Public endpoint — no auth required. Returns app config the login page needs."""
settings
=
db
.
query
(
AppSettings
)
.
first
()
allow_reg
=
True
if
settings
:
allow_reg
=
settings
.
allow_registration
return
{
"allow_registration"
:
allow_reg
}
def
_user_dict
(
u
:
User
,
perms
:
dict
=
None
)
->
dict
:
d
=
{
"id"
:
u
.
id
,
...
...
frontend/src/api.js
View file @
705bdcba
// ── Registration toggle (append to end of file) ──
const
BASE
=
"/api"
;
export
const
getAuthConfig
=
()
=>
fetch
(
`
${
BASE
}
/auth/config`
).
then
((
r
)
=>
r
.
json
());
function
headers
(
token
)
{
const
h
=
{
"Content-Type"
:
"application/json"
};
if
(
token
)
h
[
"Authorization"
]
=
`Bearer
${
token
}
`
;
return
h
;
}
export
const
getRegistrationSetting
=
(
token
)
=>
request
(
"GET"
,
"/admin/registration"
,
token
);
function
authHeader
(
token
)
{
return
token
?
{
Authorization
:
`Bearer
${
token
}
`
}
:
{};
}
export
const
toggleRegistration
=
(
token
)
=>
request
(
"PUT"
,
"/admin/registration"
,
token
);
\ No newline at end of file
async
function
request
(
method
,
path
,
token
,
body
)
{
const
opts
=
{
method
,
headers
:
headers
(
token
)
};
if
(
body
)
opts
.
body
=
JSON
.
stringify
(
body
);
const
res
=
await
fetch
(
`
${
BASE
}${
path
}
`
,
opts
);
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
err
.
detail
||
err
.
message
||
"Request failed"
);
}
return
res
.
json
();
}
export
const
login
=
(
username
,
password
)
=>
request
(
"POST"
,
"/auth/login"
,
null
,
{
username
,
password
});
export
const
register
=
(
username
,
email
,
password
)
=>
request
(
"POST"
,
"/auth/register"
,
null
,
{
username
,
email
,
password
});
export
const
getMe
=
(
token
)
=>
request
(
"GET"
,
"/auth/me"
,
token
);
export
const
getPublicConfig
=
()
=>
request
(
"GET"
,
"/auth/config"
,
null
);
export
const
listChats
=
(
token
)
=>
request
(
"GET"
,
"/chats"
,
token
);
export
const
createChat
=
(
token
,
data
=
{})
=>
request
(
"POST"
,
"/chats"
,
token
,
data
);
export
const
updateChat
=
(
token
,
chatId
,
data
)
=>
request
(
"PUT"
,
`/chats/
${
chatId
}
`
,
token
,
data
);
export
const
renameChat
=
(
token
,
chatId
,
title
)
=>
updateChat
(
token
,
chatId
,
{
title
});
export
const
deleteChat
=
(
token
,
chatId
)
=>
request
(
"DELETE"
,
`/chats/
${
chatId
}
`
,
token
);
export
const
getMessages
=
(
token
,
chatId
)
=>
request
(
"GET"
,
`/chats/
${
chatId
}
/messages`
,
token
);
export
async
function
*
streamMessage
(
token
,
chatId
,
body
,
signal
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/messages`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
(
body
),
signal
,
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
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
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
);
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
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
]);
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
adminGetSettings
=
(
token
)
=>
request
(
"GET"
,
"/admin/settings"
,
token
);
export
const
adminUpdateSettings
=
(
token
,
data
)
=>
request
(
"PUT"
,
"/admin/settings"
,
token
,
data
);
export
async
function
downloadZip
(
token
,
markdown
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/files/download-zip`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
}),
});
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
);
}
}
\ No newline at end of file
frontend/src/pages/AdminPage.jsx
View file @
705bdcba
import
React
,
{
useState
,
useEffect
,
useCallback
}
from
"react"
;
import
{
useNavigate
}
from
"react-router-dom"
;
import
{
useApp
}
from
"../store"
;
import
{
useNavigate
}
from
"react-router-dom"
;
import
{
adminStats
,
adminListUsers
,
adminCreateUser
,
adminUpdateUser
,
adminDeleteUser
,
adminGetUserPermissions
,
adminUpdateUserPermissions
,
adminGetDefaultPermissions
,
adminUpdateDefaultPermissions
,
adminApplyDefaults
,
adminStats
,
adminListUsers
,
adminCreateUser
,
adminUpdateUser
,
adminDeleteUser
,
adminListChats
,
adminGetSettings
,
adminUpdateSettings
,
}
from
"../api"
;
import
{
ArrowLeft
,
Users
,
MessageSquare
,
Database
,
Zap
,
UserPlus
,
Trash2
,
Shield
,
ShieldOff
,
Save
,
X
,
Settings2
,
Check
,
Globe
,
Paintbrush
,
BookOpen
,
GitBranch
,
Paperclip
,
Presentation
,
FileOutput
,
Cpu
,
Gauge
,
Lock
,
Loader2
,
RotateCcw
,
Copy
,
ArrowLeft
,
Users
,
MessageSquare
,
Database
,
Zap
,
Plus
,
Trash2
,
Edit
,
Shield
,
ShieldCheck
,
ShieldX
,
RefreshCw
,
ToggleLeft
,
ToggleRight
,
Settings
,
UserPlus
,
}
from
"lucide-react"
;
const
MODELS
=
[
{
id
:
"eu.anthropic.claude-opus-4-6-v1"
,
label
:
"Opus 4.6"
,
tier
:
"💰 Expensive"
},
{
id
:
"eu.anthropic.claude-haiku-4-5-20251001-v1:0"
,
label
:
"Haiku 4.5"
,
tier
:
"⚡ Cheap"
},
];
const
FEATURE_DEFS
=
[
{
key
:
"can_use_web_search"
,
label
:
"Web Search"
,
icon
:
Globe
,
color
:
"text-green-400"
,
desc
:
"SerpAPI calls — costs money per search"
},
{
key
:
"can_use_ui_design"
,
label
:
"UI Design Mode"
,
icon
:
Paintbrush
,
color
:
"text-blue-400"
,
desc
:
"Generates full HTML — uses more tokens"
},
{
key
:
"can_use_knowledge_base"
,
label
:
"Knowledge Bases"
,
icon
:
BookOpen
,
color
:
"text-emerald-400"
,
desc
:
"RAG document upload & query"
},
{
key
:
"can_use_gitlab"
,
label
:
"GitLab Access"
,
icon
:
GitBranch
,
color
:
"text-orange-400"
,
desc
:
"Repository linking & commits"
},
{
key
:
"can_use_attachments"
,
label
:
"File Attachments"
,
icon
:
Paperclip
,
color
:
"text-cyan-400"
,
desc
:
"Upload images, videos, documents"
},
{
key
:
"can_export_pptx"
,
label
:
"Export PPTX"
,
icon
:
Presentation
,
color
:
"text-amber-400"
,
desc
:
"Download as PowerPoint"
},
{
key
:
"can_export_docx"
,
label
:
"Export DOCX"
,
icon
:
FileOutput
,
color
:
"text-indigo-400"
,
desc
:
"Download as Word document"
},
];
const
LIMIT_DEFS
=
[
{
key
:
"max_tokens_cap"
,
label
:
"Max Output Tokens"
,
min
:
256
,
max
:
65536
,
step
:
256
,
desc
:
"Maximum output tokens user can set"
},
{
key
:
"max_reasoning_budget"
,
label
:
"Max Reasoning Budget"
,
min
:
0
,
max
:
32000
,
step
:
500
,
desc
:
"0 = reasoning disabled"
},
{
key
:
"max_chats"
,
label
:
"Max Chats"
,
min
:
0
,
max
:
1000
,
step
:
1
,
desc
:
"0 = unlimited"
},
{
key
:
"max_messages_per_day"
,
label
:
"Messages / Day"
,
min
:
0
,
max
:
10000
,
step
:
10
,
desc
:
"0 = unlimited"
},
{
key
:
"max_knowledge_bases"
,
label
:
"Max Knowledge Bases"
,
min
:
0
,
max
:
100
,
step
:
1
,
desc
:
"0 = unlimited"
},
{
key
:
"max_documents_per_kb"
,
label
:
"Max Docs per KB"
,
min
:
0
,
max
:
500
,
step
:
5
,
desc
:
"0 = unlimited"
},
{
key
:
"max_attachment_size_mb"
,
label
:
"Attachment Size (MB)"
,
min
:
1
,
max
:
100
,
step
:
1
,
desc
:
"Per-file upload limit"
},
{
key
:
"max_attachments_per_message"
,
label
:
"Attachments / Message"
,
min
:
1
,
max
:
50
,
step
:
1
,
desc
:
"Files per single message"
},
];
function
Toggle
({
value
,
onChange
})
{
return
(
<
button
onClick=
{
()
=>
onChange
(
!
value
)
}
className=
{
`w-10 h-5 rounded-full transition-colors relative ${value ? "bg-green-500" : "bg-anton-border"}`
}
>
<
div
className=
{
`w-4 h-4 rounded-full bg-white shadow absolute top-0.5 transition-transform ${value ? "translate-x-5.5 left-0.5" : "left-0.5"}`
}
style=
{
{
transform
:
value
?
"translateX(20px)"
:
"translateX(0)"
}
}
/>
</
button
>
);
}
export
default
function
AdminPage
()
{
const
{
state
}
=
useApp
();
const
navigate
=
useNavigate
();
const
[
stats
,
setStats
]
=
useState
(
null
);
const
[
users
,
setUsers
]
=
useState
([]);
const
[
chats
,
setChats
]
=
useState
([]);
const
[
tab
,
setTab
]
=
useState
(
"stats"
);
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
[
editingUser
,
setEditingUser
]
=
useState
(
null
);
const
[
error
,
setError
]
=
useState
(
""
);
// Permissions modal
const
[
permsUser
,
setPermsUser
]
=
useState
(
null
);
// user object or { id: "__defaults__", username: "Default Template" }
const
[
permsData
,
setPermsData
]
=
useState
(
null
);
const
[
permsSaving
,
setPermsSaving
]
=
useState
(
false
);
const
[
permsLoading
,
setPermsLoading
]
=
useState
(
false
);
const
[
applyingDefaults
,
setApplyingDefaults
]
=
useState
(
false
);
const
[
appSettings
,
setAppSettings
]
=
useState
({
allow_registration
:
true
});
const
[
settingsLoading
,
setSettingsLoading
]
=
useState
(
false
);
const
load
=
useCallback
(
async
()
=>
{
try
{
const
[
s
,
u
]
=
await
Promise
.
all
([
adminStats
(
state
.
token
),
adminListUsers
(
state
.
token
)]);
setStats
(
s
);
setUsers
(
u
);
}
catch
(
err
)
{
setError
(
err
.
message
);
}
const
[
s
,
u
,
c
]
=
await
Promise
.
all
([
adminStats
(
state
.
token
),
adminListUsers
(
state
.
token
),
adminListChats
(
state
.
token
),
]);
setStats
(
s
);
setUsers
(
u
);
setChats
(
c
);
}
catch
(
err
)
{
setError
(
err
.
message
);
}
},
[
state
.
token
]);
useEffect
(()
=>
{
load
();
},
[
load
]);
async
function
handleCreate
(
e
)
{
e
.
preventDefault
();
try
{
await
adminCreateUser
(
state
.
token
,
newUser
);
setShowCreate
(
false
);
setNewUser
({
username
:
""
,
email
:
""
,
password
:
""
,
role
:
"user"
,
quota_tokens_monthly
:
2000000
});
load
();
}
catch
(
err
)
{
setError
(
err
.
message
);
}
}
async
function
handleSaveEdit
(
userId
)
{
try
{
await
adminUpdateUser
(
state
.
token
,
userId
,
editData
);
setEditId
(
null
);
load
();
}
catch
(
err
)
{
setError
(
err
.
message
);
}
}
const
loadSettings
=
useCallback
(
async
()
=>
{
try
{
const
s
=
await
adminGetSettings
(
state
.
token
);
setAppSettings
(
s
);
}
catch
{
}
},
[
state
.
token
]);
async
function
handleDelete
(
userId
,
username
)
{
if
(
!
confirm
(
`Delete user "
${
username
}
"? This is permanent.`
))
return
;
try
{
await
adminDeleteUser
(
state
.
token
,
userId
);
load
();
}
catch
(
err
)
{
setError
(
err
.
message
);
}
}
useEffect
(()
=>
{
load
();
loadSettings
();
},
[
load
,
loadSettings
]);
// ═══ Permissions ═══
async
function
openPerms
(
user
)
{
setPermsUser
(
user
);
setPermsLoading
(
true
);
setPermsData
(
null
);
async
function
handleCreateUser
()
{
try
{
const
data
=
user
.
id
===
"__defaults__"
?
await
adminGetDefaultPermissions
(
state
.
token
)
:
await
adminGetUserPermissions
(
state
.
token
,
user
.
id
);
setPermsData
(
data
);
}
catch
(
err
)
{
setError
(
err
.
message
);
setPermsUser
(
null
);
}
setPermsLoading
(
false
);
await
adminCreateUser
(
state
.
token
,
newUser
);
setShowCreate
(
false
);
setNewUser
({
username
:
""
,
email
:
""
,
password
:
""
,
role
:
"user"
,
quota_tokens_monthly
:
2000000
});
load
();
}
catch
(
err
)
{
setError
(
err
.
message
);
}
}
async
function
savePerms
()
{
if
(
!
permsUser
||
!
permsData
)
return
;
setPermsSaving
(
true
);
async
function
handleUpdateUser
(
userId
,
data
)
{
try
{
if
(
permsUser
.
id
===
"__defaults__"
)
{
await
adminUpdateDefaultPermissions
(
state
.
token
,
permsData
);
}
else
{
await
adminUpdateUserPermissions
(
state
.
token
,
permsUser
.
id
,
permsData
);
}
setPermsUser
(
null
);
setPermsData
(
null
);
await
adminUpdateUser
(
state
.
token
,
userId
,
data
);
setEditingUser
(
null
);
load
();
}
catch
(
err
)
{
setError
(
err
.
message
);
}
setPermsSaving
(
false
);
}
async
function
handleApplyDefaults
()
{
if
(
!
confirm
(
"Apply default permissions to ALL regular users? This will overwrite their current permissions."
))
return
;
setApplyingDefaults
(
true
);
async
function
handleDeleteUser
(
userId
)
{
if
(
!
confirm
(
"Delete this user and all their data?"
))
return
;
try
{
const
res
=
await
adminApplyDefaults
(
state
.
token
);
alert
(
`✅ Applied to
${
res
.
users_updated
}
users`
);
await
adminDeleteUser
(
state
.
token
,
userId
);
load
(
);
}
catch
(
err
)
{
setError
(
err
.
message
);
}
setApplyingDefaults
(
false
);
}
function
updatePerm
(
key
,
value
)
{
setPermsData
(
prev
=>
({
...
prev
,
[
key
]:
value
}));
}
function
formatNum
(
n
)
{
if
(
n
>=
1
_000_000
)
return
(
n
/
1
_000_000
).
toFixed
(
1
)
+
"M"
;
if
(
n
>=
1
_000
)
return
(
n
/
1
_000
).
toFixed
(
0
)
+
"K"
;
return
String
(
n
);
async
function
toggleRegistration
()
{
setSettingsLoading
(
true
);
try
{
const
res
=
await
adminUpdateSettings
(
state
.
token
,
{
allow_registration
:
!
appSettings
.
allow_registration
,
});
setAppSettings
(
res
);
}
catch
(
err
)
{
setError
(
err
.
message
);
}
setSettingsLoading
(
false
);
}
if
(
state
.
user
?.
role
!==
"superadmin"
)
{
return
<
div
className=
"h-full flex items-center justify-center"
><
p
className=
"text-anton-danger text-lg"
>
⛔ Access Denied
</
p
></
div
>;
return
(
<
div
className=
"h-dvh flex items-center justify-center bg-anton-bg"
>
<
div
className=
"text-center"
>
<
ShieldX
size=
{
48
}
className=
"text-red-500 mx-auto mb-4"
/>
<
h2
className=
"text-xl text-white font-bold"
>
Access Denied
</
h2
>
<
p
className=
"text-anton-muted mt-2"
>
Superadmin access required.
</
p
>
<
button
onClick=
{
()
=>
navigate
(
"/"
)
}
className=
"mt-4 text-anton-accent hover:underline"
>
← Back to chat
</
button
>
</
div
>
</
div
>
);
}
return
(
<
div
className=
"h-full overflow-y-auto bg-anton-bg p-4 sm:p-6"
>
<
div
className=
"max-w-6xl mx-auto space-y-6 animate-fade-in"
>
{
/* Header */
}
<
div
className=
"flex items-center gap-4"
>
<
button
onClick=
{
()
=>
navigate
(
"/"
)
}
className=
"p-2 rounded-lg bg-anton-surface border border-anton-border hover:border-anton-accent transition"
><
ArrowLeft
size=
{
20
}
/></
button
>
<
div
>
<
h1
className=
"text-2xl font-bold text-white flex items-center gap-2"
><
Shield
size=
{
24
}
className=
"text-anton-accent"
/>
Admin Panel
</
h1
>
<
p
className=
"text-anton-muted text-sm"
>
Users, permissions
&
cost control
</
p
>
</
div
>
<
div
className=
"h-dvh flex flex-col bg-anton-bg"
>
{
/* Header */
}
<
div
className=
"border-b border-anton-border bg-anton-surface px-4 py-3 flex items-center gap-3"
>
<
button
onClick=
{
()
=>
navigate
(
"/"
)
}
className=
"text-anton-muted hover:text-white"
><
ArrowLeft
size=
{
20
}
/></
button
>
<
ShieldCheck
size=
{
20
}
className=
"text-anton-accent"
/>
<
h1
className=
"text-white font-semibold"
>
Admin Dashboard
</
h1
>
<
button
onClick=
{
()
=>
{
load
();
loadSettings
();
}
}
className=
"ml-auto text-anton-muted hover:text-white"
><
RefreshCw
size=
{
16
}
/></
button
>
</
div
>
{
error
&&
(
<
div
className=
"mx-4 mt-2 bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 text-red-400 text-sm"
>
{
error
}
<
button
onClick=
{
()
=>
setError
(
""
)
}
className=
"ml-2 underline"
>
dismiss
</
button
>
</
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
>)
}
{
/* Tabs */
}
<
div
className=
"flex gap-1 px-4 pt-3"
>
{
[
{
id
:
"stats"
,
label
:
"Stats"
,
icon
:
Zap
},
{
id
:
"settings"
,
label
:
"Settings"
,
icon
:
Settings
},
{
id
:
"users"
,
label
:
"Users"
,
icon
:
Users
},
{
id
:
"chats"
,
label
:
"Chats"
,
icon
:
MessageSquare
},
].
map
((
t
)
=>
(
<
button
key=
{
t
.
id
}
onClick=
{
()
=>
setTab
(
t
.
id
)
}
className=
{
`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm 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
>
{
/* Stats */
}
{
stats
&&
(
<
div
className=
"grid grid-cols-2 md:grid-cols-4 gap-4"
>
{
/* Content */
}
<
div
className=
"flex-1 overflow-y-auto p-4"
>
{
/* ── STATS TAB ── */
}
{
tab
===
"stats"
&&
stats
&&
(
<
div
className=
"grid grid-cols-2 md:grid-cols-3 gap-3"
>
{
[
{
label
:
"Users"
,
value
:
stats
.
total_users
,
icon
:
Users
,
color
:
"text-blue-400"
},
{
label
:
"Chats"
,
value
:
stats
.
total_chats
,
icon
:
MessageSquare
,
color
:
"text-green-400"
},
{
label
:
"Messages"
,
value
:
formatNum
(
stats
.
total_messages
),
icon
:
Zap
,
color
:
"text-anton-accent"
},
{
label
:
"Tokens Used"
,
value
:
formatNum
(
stats
.
total_tokens_used
),
icon
:
Database
,
color
:
"text-purple-400"
},
].
map
((
s
)
=>
(
<
div
key=
{
s
.
label
}
className=
"bg-anton-surface border border-anton-border rounded-xl p-4"
>
<
div
className=
"flex items-center gap-2 mb-1"
><
s
.
icon
size=
{
16
}
className=
{
s
.
color
}
/><
span
className=
"text-anton-muted text-sm"
>
{
s
.
label
}
</
span
></
div
>
<
p
className=
"text-2xl font-bold text-white"
>
{
s
.
value
}
</
p
>
{
label
:
"Active"
,
value
:
stats
.
active_users
,
icon
:
ShieldCheck
,
color
:
"text-green-400"
},
{
label
:
"Chats"
,
value
:
stats
.
total_chats
,
icon
:
MessageSquare
,
color
:
"text-purple-400"
},
{
label
:
"Messages"
,
value
:
stats
.
total_messages
,
icon
:
MessageSquare
,
color
:
"text-yellow-400"
},
{
label
:
"Tokens Used"
,
value
:
(
stats
.
total_tokens_used
||
0
).
toLocaleString
(),
icon
:
Zap
,
color
:
"text-red-400"
},
{
label
:
"Knowledge Bases"
,
value
:
stats
.
total_knowledge_bases
,
icon
:
Database
,
color
:
"text-cyan-400"
},
].
map
((
s
,
i
)
=>
(
<
div
key=
{
i
}
className=
"bg-anton-card border border-anton-border rounded-xl p-4"
>
<
div
className=
"flex items-center gap-2 mb-2"
>
<
s
.
icon
size=
{
16
}
className=
{
s
.
color
}
/>
<
span
className=
"text-xs text-anton-muted"
>
{
s
.
label
}
</
span
>
</
div
>
<
div
className=
"text-2xl font-bold text-white"
>
{
s
.
value
}
</
div
>
</
div
>
))
}
</
div
>
)
}
{
/* User Management */
}
<
div
className=
"bg-anton-surface border border-anton-border rounded-xl overflow-hidden"
>
<
div
className=
"px-5 py-4 border-b border-anton-border flex items-center justify-between flex-wrap gap-2"
>
<
h2
className=
"text-lg font-semibold text-white"
>
Users
</
h2
>
<
div
className=
"flex items-center gap-2"
>
<
button
onClick=
{
()
=>
openPerms
({
id
:
"__defaults__"
,
username
:
"Default Template"
})
}
className=
"flex items-center gap-1.5 px-3 py-1.5 bg-purple-500/20 text-purple-400 border border-purple-500/30 rounded-lg text-sm font-medium hover:bg-purple-500/30 transition"
title=
"Edit default permissions for new users"
>
<
Lock
size=
{
14
}
/>
Defaults
</
button
>
<
button
onClick=
{
handleApplyDefaults
}
disabled=
{
applyingDefaults
}
className=
"flex items-center gap-1.5 px-3 py-1.5 bg-anton-card border border-anton-border text-anton-muted rounded-lg text-sm hover:text-white transition disabled:opacity-50"
>
{
applyingDefaults
?
<
Loader2
size=
{
14
}
className=
"animate-spin"
/>
:
<
Copy
size=
{
14
}
/>
}
Apply to All
</
button
>
<
button
onClick=
{
()
=>
setShowCreate
(
!
showCreate
)
}
className=
"flex items-center gap-1.5 px-3 py-1.5 bg-anton-accent text-white rounded-lg text-sm font-medium hover:opacity-90 transition"
>
{
showCreate
?
<
X
size=
{
14
}
/>
:
<
UserPlus
size=
{
14
}
/>
}
{
showCreate
?
"Cancel"
:
"New User"
}
</
button
>
</
div
>
</
div
>
{
showCreate
&&
(
<
form
onSubmit=
{
handleCreate
}
className=
"px-5 py-4 border-b border-anton-border bg-anton-card grid grid-cols-1 md:grid-cols-3 gap-3"
>
<
input
placeholder=
"Username"
required
value=
{
newUser
.
username
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
username
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
input
placeholder=
"Email"
type=
"email"
required
value=
{
newUser
.
email
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
email
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
input
placeholder=
"Password"
required
value=
{
newUser
.
password
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
password
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
select
value=
{
newUser
.
role
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
role
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
>
<
option
value=
"user"
>
User
</
option
><
option
value=
"admin"
>
Admin
</
option
><
option
value=
"superadmin"
>
Superadmin
</
option
>
</
select
>
<
input
placeholder=
"Monthly quota"
type=
"number"
value=
{
newUser
.
quota_tokens_monthly
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
quota_tokens_monthly
:
Number
(
e
.
target
.
value
)
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
button
type=
"submit"
className=
"bg-anton-success text-white rounded-lg px-3 py-2 text-sm font-medium hover:opacity-90"
>
Create
</
button
>
</
form
>
)
}
{
/* ── SETTINGS TAB ── */
}
{
tab
===
"settings"
&&
(
<
div
className=
"space-y-4 max-w-lg"
>
<
h2
className=
"text-lg font-semibold text-white flex items-center gap-2"
>
<
Settings
size=
{
18
}
className=
"text-anton-accent"
/>
App Settings
</
h2
>
<
div
className=
"overflow-x-auto"
>
<
table
className=
"w-full text-sm"
>
<
thead
><
tr
className=
"text-left text-anton-muted border-b border-anton-border"
>
<
th
className=
"px-5 py-3"
>
User
</
th
><
th
className=
"px-5 py-3"
>
Role
</
th
><
th
className=
"px-5 py-3"
>
Quota
</
th
><
th
className=
"px-5 py-3"
>
Used
</
th
><
th
className=
"px-5 py-3"
>
Chats
</
th
><
th
className=
"px-5 py-3"
>
Status
</
th
><
th
className=
"px-5 py-3"
>
Actions
</
th
>
</
tr
></
thead
>
<
tbody
>
{
users
.
map
((
u
)
=>
(
<
tr
key=
{
u
.
id
}
className=
"border-b border-anton-border/50 hover:bg-anton-card/50 transition"
>
<
td
className=
"px-5 py-3"
><
div
className=
"text-white font-medium"
>
{
u
.
username
}
</
div
><
div
className=
"text-anton-muted text-xs"
>
{
u
.
email
}
</
div
></
td
>
<
td
className=
"px-5 py-3"
>
{
editId
===
u
.
id
?
(
<
select
value=
{
editData
.
role
??
u
.
role
}
onChange=
{
(
e
)
=>
setEditData
({
...
editData
,
role
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs"
>
<
option
value=
"user"
>
user
</
option
><
option
value=
"admin"
>
admin
</
option
><
option
value=
"superadmin"
>
superadmin
</
option
>
</
select
>
)
:
(
<
span
className=
{
`px-2 py-0.5 rounded-full text-xs font-medium ${u.role === "superadmin" ? "bg-anton-accent/20 text-anton-accent" : u.role === "admin" ? "bg-blue-500/20 text-blue-400" : "bg-anton-border text-anton-muted"}`
}
>
{
u
.
role
}
</
span
>
)
}
</
td
>
<
td
className=
"px-5 py-3 text-anton-muted"
>
{
editId
===
u
.
id
?
<
input
type=
"number"
value=
{
editData
.
quota_tokens_monthly
??
u
.
quota_tokens_monthly
}
onChange=
{
(
e
)
=>
setEditData
({
...
editData
,
quota_tokens_monthly
:
Number
(
e
.
target
.
value
)
})
}
className=
"bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs w-28"
/>
:
formatNum
(
u
.
quota_tokens_monthly
)
}
</
td
>
<
td
className=
"px-5 py-3 text-anton-muted"
>
{
formatNum
(
u
.
tokens_used_this_month
)
}
</
td
>
<
td
className=
"px-5 py-3 text-anton-muted"
>
{
u
.
chat_count
}
</
td
>
<
td
className=
"px-5 py-3"
><
span
className=
{
`w-2 h-2 inline-block rounded-full mr-1 ${u.is_active ? "bg-anton-success" : "bg-anton-danger"}`
}
/><
span
className=
"text-xs text-anton-muted"
>
{
u
.
is_active
?
"Active"
:
"Off"
}
</
span
></
td
>
<
td
className=
"px-5 py-3"
>
<
div
className=
"flex items-center gap-1"
>
{
editId
===
u
.
id
?
(
<><
button
onClick=
{
()
=>
handleSaveEdit
(
u
.
id
)
}
className=
"p-1 rounded hover:bg-anton-success/20 text-anton-success"
><
Save
size=
{
14
}
/></
button
><
button
onClick=
{
()
=>
setEditId
(
null
)
}
className=
"p-1 rounded hover:bg-anton-border text-anton-muted"
><
X
size=
{
14
}
/></
button
></>
)
:
(
<>
{
u
.
role
!==
"superadmin"
&&
(
<
button
onClick=
{
()
=>
openPerms
(
u
)
}
className=
"p-1 rounded hover:bg-purple-500/20 text-purple-400"
title=
"Permissions"
><
Settings2
size=
{
14
}
/></
button
>
)
}
<
button
onClick=
{
()
=>
{
setEditId
(
u
.
id
);
setEditData
({});
}
}
className=
"p-1 rounded hover:bg-anton-accent/20 text-anton-accent text-xs"
>
Edit
</
button
>
<
button
onClick=
{
()
=>
adminUpdateUser
(
state
.
token
,
u
.
id
,
{
is_active
:
!
u
.
is_active
}).
then
(
load
)
}
className=
"p-1 rounded hover:bg-anton-border text-anton-muted"
title=
{
u
.
is_active
?
"Disable"
:
"Enable"
}
>
{
u
.
is_active
?
<
ShieldOff
size=
{
14
}
/>
:
<
Shield
size=
{
14
}
/>
}
</
button
>
{
u
.
role
!==
"superadmin"
&&
<
button
onClick=
{
()
=>
handleDelete
(
u
.
id
,
u
.
username
)
}
className=
"p-1 rounded hover:bg-red-500/20 text-anton-danger"
><
Trash2
size=
{
14
}
/></
button
>
}
</>
)
}
</
div
>
</
td
>
</
tr
>
))
}
</
tbody
>
</
table
>
<
div
className=
"bg-anton-card border border-anton-border rounded-xl p-5"
>
<
div
className=
"flex items-center justify-between"
>
<
div
>
<
h3
className=
"text-white font-medium flex items-center gap-2"
>
<
UserPlus
size=
{
16
}
className=
"text-blue-400"
/>
User Registration
</
h3
>
<
p
className=
"text-anton-muted text-sm mt-1"
>
{
appSettings
.
allow_registration
?
"Anyone can create an account on the login page."
:
"Registration is disabled. Only admins can create users."
}
</
p
>
</
div
>
<
button
onClick=
{
toggleRegistration
}
disabled=
{
settingsLoading
}
className=
{
`shrink-0 transition ${settingsLoading ? "opacity-50" : "hover:opacity-80"}`
}
title=
{
appSettings
.
allow_registration
?
"Click to disable"
:
"Click to enable"
}
>
{
appSettings
.
allow_registration
?
(
<
ToggleRight
size=
{
40
}
className=
"text-green-400"
/>
)
:
(
<
ToggleLeft
size=
{
40
}
className=
"text-anton-muted"
/>
)
}
</
button
>
</
div
>
</
div
>
</
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
>
{
/* ── USERS TAB ── */
}
{
tab
===
"users"
&&
(
<
div
className=
"space-y-3"
>
<
div
className=
"flex items-center justify-between"
>
<
h2
className=
"text-lg font-semibold text-white"
>
Users (
{
users
.
length
}
)
</
h2
>
<
button
onClick=
{
()
=>
setShowCreate
(
!
showCreate
)
}
className=
"flex items-center gap-1.5 bg-anton-accent text-white px-3 py-1.5 rounded-lg text-sm hover:opacity-80"
>
<
Plus
size=
{
14
}
/>
Create User
</
button
>
</
div
>
{
permsLoading
?
(
<
div
className=
"flex items-center justify-center py-20"
><
Loader2
size=
{
24
}
className=
"text-anton-accent animate-spin"
/></
div
>
)
:
permsData
?
(
<
div
className=
"p-6 space-y-6 max-h-[70vh] overflow-y-auto"
>
{
/* Feature Access */
}
<
div
>
<
h3
className=
"text-sm font-semibold text-white mb-3 flex items-center gap-2"
><
Lock
size=
{
14
}
className=
"text-anton-accent"
/>
Feature Access
</
h3
>
<
div
className=
"space-y-2"
>
{
FEATURE_DEFS
.
map
(
f
=>
{
const
Icon
=
f
.
icon
;
return
(
<
div
key=
{
f
.
key
}
className=
"flex items-center justify-between bg-anton-card rounded-lg px-4 py-3 border border-anton-border"
>
<
div
className=
"flex items-center gap-3"
>
<
Icon
size=
{
16
}
className=
{
f
.
color
}
/>
<
div
>
<
p
className=
"text-sm text-white font-medium"
>
{
f
.
label
}
</
p
>
<
p
className=
"text-[10px] text-anton-muted"
>
{
f
.
desc
}
</
p
>
</
div
>
</
div
>
<
Toggle
value=
{
!!
permsData
[
f
.
key
]
}
onChange=
{
v
=>
updatePerm
(
f
.
key
,
v
)
}
/>
</
div
>
);
})
}
</
div
>
{
showCreate
&&
(
<
div
className=
"bg-anton-card border border-anton-border rounded-xl p-4 space-y-3 animate-fade-in"
>
<
h3
className=
"text-sm font-semibold text-white"
>
New User
</
h3
>
<
div
className=
"grid grid-cols-2 gap-3"
>
<
input
placeholder=
"Username"
value=
{
newUser
.
username
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
username
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
input
placeholder=
"Email"
value=
{
newUser
.
email
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
email
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
input
placeholder=
"Password"
type=
"password"
value=
{
newUser
.
password
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
password
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
<
select
value=
{
newUser
.
role
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
role
:
e
.
target
.
value
})
}
className=
"bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
>
<
option
value=
"user"
>
User
</
option
>
<
option
value=
"admin"
>
Admin
</
option
>
</
select
>
</
div
>
<
div
className=
"flex gap-2"
>
<
button
onClick=
{
handleCreateUser
}
className=
"bg-anton-accent text-white px-4 py-1.5 rounded-lg text-sm hover:opacity-80"
>
Create
</
button
>
<
button
onClick=
{
()
=>
setShowCreate
(
false
)
}
className=
"text-anton-muted text-sm hover:text-white"
>
Cancel
</
button
>
</
div
>
</
div
>
)
}
{
/* Model Access */
}
<
div
>
<
h3
className=
"text-sm font-semibold text-white mb-3 flex items-center gap-2"
><
Cpu
size=
{
14
}
className=
"text-cyan-400"
/>
Model Access
</
h3
>
<
div
className=
"space-y-2"
>
<
label
className=
"flex items-center gap-3 bg-anton-card rounded-lg px-4 py-3 border border-anton-border cursor-pointer"
>
<
input
type=
"checkbox"
checked=
{
permsData
.
allowed_models
===
"all"
}
onChange=
{
e
=>
updatePerm
(
"allowed_models"
,
e
.
target
.
checked
?
"all"
:
MODELS
.
map
(
m
=>
m
.
id
).
join
(
","
))
}
className=
"w-4 h-4 rounded accent-anton-accent"
/>
<
div
><
p
className=
"text-sm text-white font-medium"
>
All Models
</
p
><
p
className=
"text-[10px] text-anton-muted"
>
Access to every available model
</
p
></
div
>
</
label
>
{
permsData
.
allowed_models
!==
"all"
&&
(
<
div
className=
"pl-4 space-y-1.5"
>
{
MODELS
.
map
(
m
=>
{
const
list
=
(
permsData
.
allowed_models
||
""
).
split
(
","
).
map
(
s
=>
s
.
trim
()).
filter
(
Boolean
);
const
checked
=
list
.
includes
(
m
.
id
);
return
(
<
label
key=
{
m
.
id
}
className=
"flex items-center gap-3 bg-anton-bg rounded-lg px-3 py-2 border border-anton-border/50 cursor-pointer"
>
<
input
type=
"checkbox"
checked=
{
checked
}
onChange=
{
e
=>
{
let
next
=
checked
?
list
.
filter
(
x
=>
x
!==
m
.
id
)
:
[...
list
,
m
.
id
];
if
(
!
next
.
length
)
next
=
[
m
.
id
];
// Can't have zero models
updatePerm
(
"allowed_models"
,
next
.
join
(
","
));
}
}
className=
"w-4 h-4 rounded accent-anton-accent"
/>
<
div
><
span
className=
"text-sm text-white"
>
{
m
.
label
}
</
span
><
span
className=
"text-[10px] text-anton-muted ml-2"
>
{
m
.
tier
}
</
span
></
div
>
</
label
>
);
})
}
</
div
>
<
div
className=
"space-y-2"
>
{
users
.
map
((
u
)
=>
(
<
div
key=
{
u
.
id
}
className=
"bg-anton-card border border-anton-border rounded-xl p-4 flex items-center gap-4"
>
<
div
className=
"flex-1 min-w-0"
>
<
div
className=
"flex items-center gap-2"
>
<
span
className=
"text-white font-medium"
>
{
u
.
username
}
</
span
>
<
span
className=
{
`text-[10px] px-1.5 py-0.5 rounded ${u.role === "superadmin" ? "bg-red-500/20 text-red-400" : u.role === "admin" ? "bg-yellow-500/20 text-yellow-400" : "bg-blue-500/20 text-blue-400"}`
}
>
{
u
.
role
}
</
span
>
{
!
u
.
is_active
&&
<
span
className=
"text-[10px] px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400"
>
disabled
</
span
>
}
</
div
>
<
div
className=
"text-xs text-anton-muted mt-0.5"
>
{
u
.
email
}
·
{
u
.
chat_count
}
chats ·
{
(
u
.
tokens_used_this_month
||
0
).
toLocaleString
()
}
tokens used
</
div
>
</
div
>
<
div
className=
"flex items-center gap-1"
>
{
u
.
role
!==
"superadmin"
&&
(
<>
<
button
onClick=
{
()
=>
handleUpdateUser
(
u
.
id
,
{
is_active
:
!
u
.
is_active
})
}
className=
{
`p-1.5 rounded-lg transition ${u.is_active ? "text-green-400 hover:bg-green-500/10" : "text-red-400 hover:bg-red-500/10"}`
}
title=
{
u
.
is_active
?
"Disable"
:
"Enable"
}
>
{
u
.
is_active
?
<
ShieldCheck
size=
{
16
}
/>
:
<
ShieldX
size=
{
16
}
/>
}
</
button
>
<
button
onClick=
{
()
=>
handleDeleteUser
(
u
.
id
)
}
className=
"p-1.5 rounded-lg text-red-400 hover:bg-red-500/10 transition"
title=
"Delete"
>
<
Trash2
size=
{
16
}
/>
</
button
>
</>
)
}
</
div
>
</
div
>
))
}
</
div
>
</
div
>
)
}
{
/* Limits */
}
<
div
>
<
h3
className=
"text-sm font-semibold text-white mb-3 flex items-center gap-2"
><
Gauge
size=
{
14
}
className=
"text-amber-400"
/>
Usage Limits
</
h3
>
<
div
className=
"grid grid-cols-1 sm:grid-cols-2 gap-3"
>
{
LIMIT_DEFS
.
map
(
l
=>
(
<
div
key=
{
l
.
key
}
className=
"bg-anton-card rounded-lg px-4 py-3 border border-anton-border"
>
<
div
className=
"flex justify-between items-center mb-1"
>
<
label
className=
"text-xs text-anton-muted"
>
{
l
.
label
}
</
label
>
<
span
className=
"text-xs text-white font-mono"
>
{
(
permsData
[
l
.
key
]
||
0
)
===
0
&&
l
.
desc
?.
includes
(
"unlimited"
)
?
"∞"
:
(
permsData
[
l
.
key
]
||
0
).
toLocaleString
()
}
</
span
>
</
div
>
<
input
type=
"number"
min=
{
l
.
min
}
max=
{
l
.
max
}
step=
{
l
.
step
}
value=
{
permsData
[
l
.
key
]
||
0
}
onChange=
{
e
=>
updatePerm
(
l
.
key
,
Math
.
min
(
Math
.
max
(
Number
(
e
.
target
.
value
)
||
0
,
l
.
min
),
l
.
max
))
}
className=
"w-full bg-anton-bg border border-anton-border rounded px-2.5 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-anton-accent"
/>
<
p
className=
"text-[9px] text-anton-muted mt-1"
>
{
l
.
desc
}
</
p
>
</
div
>
))
}
</
div
>
{
/* ── CHATS TAB ── */
}
{
tab
===
"chats"
&&
(
<
div
className=
"space-y-2"
>
<
h2
className=
"text-lg font-semibold text-white"
>
Recent Chats (
{
chats
.
length
}
)
</
h2
>
{
chats
.
map
((
c
)
=>
(
<
div
key=
{
c
.
id
}
className=
"bg-anton-card border border-anton-border rounded-xl p-3 flex items-center gap-3"
>
<
div
className=
"flex-1 min-w-0"
>
<
div
className=
"text-white text-sm truncate"
>
{
c
.
title
}
</
div
>
<
div
className=
"text-xs text-anton-muted"
>
{
c
.
username
}
·
{
c
.
message_count
}
msgs ·
{
c
.
updated_at
}
</
div
>
</
div
>
</
div
>
)
:
null
}
{
/* Modal Footer */
}
{
permsData
&&
(
<
div
className=
"px-6 py-4 border-t border-anton-border flex items-center justify-between"
>
<
button
onClick=
{
()
=>
{
setPermsUser
(
null
);
setPermsData
(
null
);
}
}
className=
"px-4 py-2 rounded-lg border border-anton-border text-anton-muted hover:text-white transition text-sm"
>
Cancel
</
button
>
<
button
onClick=
{
savePerms
}
disabled=
{
permsSaving
}
className=
"flex items-center gap-2 px-5 py-2 bg-anton-accent text-white rounded-lg text-sm font-semibold hover:opacity-80 transition disabled:opacity-50"
>
{
permsSaving
?
<
Loader2
size=
{
14
}
className=
"animate-spin"
/>
:
<
Save
size=
{
14
}
/>
}
Save Permissions
</
button
>
</
div
>
)
}
))
}
</
div
>
</
div
>
)
}
)
}
</
div
>
</
div
>
);
}
\ No newline at end of file
frontend/src/pages/LoginPage.jsx
View file @
705bdcba
import
React
,
{
useState
,
useEffect
}
from
"react"
;
import
{
useApp
}
from
"../store"
;
import
{
login
,
register
,
get
Auth
Config
}
from
"../api"
;
import
{
Flame
,
LogIn
,
UserPlus
,
AlertCircle
}
from
"lucide-react"
;
import
{
login
,
register
,
get
Public
Config
}
from
"../api"
;
import
{
Flame
,
Eye
,
EyeOff
,
Loader2
}
from
"lucide-react"
;
export
default
function
LoginPage
()
{
const
{
dispatch
}
=
useApp
();
...
...
@@ -9,21 +9,23 @@ export default function LoginPage() {
const
[
username
,
setUsername
]
=
useState
(
""
);
const
[
email
,
setEmail
]
=
useState
(
""
);
const
[
password
,
setPassword
]
=
useState
(
""
);
const
[
showPw
,
setShowPw
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
allowRegistration
,
setAllowRegistration
]
=
useState
(
true
);
const
[
configLoaded
,
setConfigLoaded
]
=
useState
(
false
);
useEffect
(()
=>
{
getAuthConfig
()
.
then
((
data
)
=>
{
setAllowRegistration
(
data
.
allow_registration
!==
false
);
set
ConfigLoaded
(
true
);
}
)
.
catch
(()
=>
{
(
async
()
=>
{
try
{
const
cfg
=
await
getPublicConfig
(
);
set
AllowRegistration
(
cfg
.
allow_registration
);
}
catch
{
// If config fetch fails, default to allowing registration
setAllowRegistration
(
true
);
setConfigLoaded
(
true
);
});
}
setConfigLoaded
(
true
);
})();
},
[]);
async
function
handleSubmit
(
e
)
{
...
...
@@ -31,26 +33,31 @@ export default function LoginPage() {
setError
(
""
);
setLoading
(
true
);
try
{
let
res
ult
;
let
res
;
if
(
isRegister
)
{
if
(
!
allowRegistration
)
{
setError
(
"Registration is disabled."
);
setLoading
(
false
);
return
;
}
result
=
await
register
(
username
,
email
,
password
);
if
(
!
email
)
{
setError
(
"Email is required"
);
setLoading
(
false
);
return
;
}
res
=
await
register
(
username
,
email
,
password
);
}
else
{
res
ult
=
await
login
(
username
,
password
);
res
=
await
login
(
username
,
password
);
}
dispatch
({
type
:
"LOGIN"
,
token
:
result
.
token
,
user
:
result
.
user
});
dispatch
({
type
:
"SET_TOKEN"
,
token
:
res
.
token
});
dispatch
({
type
:
"SET_USER"
,
user
:
res
.
user
});
}
catch
(
err
)
{
setError
(
err
.
message
||
"Something went wrong"
);
setError
(
err
.
message
);
}
setLoading
(
false
);
}
if
(
!
configLoaded
)
{
return
(
<
div
className=
"h-dvh flex items-center justify-center bg-anton-bg"
>
<
Loader2
className=
"animate-spin text-anton-accent"
size=
{
32
}
/>
</
div
>
);
}
return
(
<
div
className=
"h-dvh flex items-center justify-center bg-anton-bg p-4"
>
<
div
className=
"h-dvh flex items-center justify-center bg-anton-bg p
x
-4"
>
<
div
className=
"w-full max-w-sm"
>
<
div
className=
"flex flex-col items-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"
>
...
...
@@ -60,85 +67,65 @@ export default function LoginPage() {
<
p
className=
"text-anton-muted text-sm mt-1"
>
Avatar of All Elements of Code
</
p
>
</
div
>
<
div
className=
"bg-anton-card border border-anton-border rounded-2xl p-6"
>
{
/* Tab buttons — only show Register tab if registration is allowed */
}
<
div
className=
"flex gap-2 mb-6"
>
<
button
onClick=
{
()
=>
{
setIsRegister
(
false
);
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"
}`
}
>
<
LogIn
size=
{
14
}
className=
"inline mr-1.5"
/>
Login
</
button
>
{
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
>
<
form
onSubmit=
{
handleSubmit
}
className=
"bg-anton-card border border-anton-border rounded-2xl p-6 space-y-4"
>
<
h2
className=
"text-lg font-semibold text-white text-center"
>
{
isRegister
?
"Create Account"
:
"Sign In"
}
</
h2
>
{
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
className=
"bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 text-red-400 text-sm"
>
{
error
}
</
div
>
)
}
<
form
onSubmit=
{
handleSubmit
}
className=
"space-y-4"
>
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Username
</
label
>
<
input
type=
"text"
value=
{
username
}
onChange=
{
(
e
)
=>
setUsername
(
e
.
target
.
value
)
}
required
autoFocus
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"
/>
</
div
>
{
isRegister
&&
(
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Username
</
label
>
<
input
type=
"text"
value=
{
username
}
onChange=
{
(
e
)
=>
setUsername
(
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=
"username"
/>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Email
</
label
>
<
input
type=
"email"
value=
{
email
}
onChange=
{
(
e
)
=>
setEmail
(
e
.
target
.
value
)
}
required
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"
/>
</
div
>
)
}
{
isRegister
&&
(
<
div
>
<
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
type=
"password"
value=
{
password
}
onChange=
{
(
e
)
=>
setPassword
(
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=
{
isRegister
?
"new-password"
:
"current-password"
}
/>
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Password
</
label
>
<
div
className=
"relative"
>
<
input
type=
{
showPw
?
"text"
:
"password"
}
value=
{
password
}
onChange=
{
(
e
)
=>
setPassword
(
e
.
target
.
value
)
}
required
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 pr-10"
/>
<
button
type=
"button"
onClick=
{
()
=>
setShowPw
(
!
showPw
)
}
className=
"absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white"
>
{
showPw
?
<
EyeOff
size=
{
16
}
/>
:
<
Eye
size=
{
16
}
/>
}
</
button
>
</
div
>
</
div
>
<
button
type=
"submit"
disabled=
{
loading
}
className=
"w-full bg-anton-accent text-white py-2.5 rounded-lg font-medium hover:opacity-90 transition disabled:opacity-50"
>
{
loading
?
"..."
:
isRegister
?
"Create Account"
:
"Sign In"
}
</
button
>
</
form
>
<
button
type=
"submit"
disabled=
{
loading
}
className=
"w-full bg-anton-accent text-white rounded-lg py-2.5 text-sm font-medium hover:opacity-90 transition disabled:opacity-50 flex items-center justify-center gap-2"
>
{
loading
&&
<
Loader2
size=
{
16
}
className=
"animate-spin"
/>
}
{
isRegister
?
"Create Account"
:
"Sign In"
}
</
button
>
{
!
allowRegistration
&&
configLoaded
&&
!
isRegister
&&
(
<
p
className=
"text-[11px] text-anton-muted text-center mt-4"
>
Registration is currently disabled by the administrator.
{
allowRegistration
&&
(
<
p
className=
"text-center text-sm text-anton-muted"
>
{
isRegister
?
"Already have an account?"
:
"Don't have an account?"
}{
" "
}
<
button
type=
"button"
onClick=
{
()
=>
{
setIsRegister
(
!
isRegister
);
setError
(
""
);
}
}
className=
"text-anton-accent hover:underline"
>
{
isRegister
?
"Sign In"
:
"Register"
}
</
button
>
</
p
>
)
}
</
div
>
{
!
allowRegistration
&&
!
isRegister
&&
(
<
p
className=
"text-center text-xs text-anton-muted"
>
Registration is disabled. Contact your administrator.
</
p
>
)
}
</
form
>
</
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