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
Show 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
(
"/registration"
)
@
router
.
get
(
"/chats"
)
def
get_registration_setting
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
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
# ═══════════════════════════════════════════════════
# 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
()
settings
=
db
.
query
(
AppSettings
)
.
first
()
if
not
settings
:
if
not
settings
:
settings
=
AppSettings
(
allow_registration
=
True
)
settings
=
AppSettings
(
allow_registration
=
True
)
db
.
add
(
settings
)
db
.
add
(
settings
)
db
.
commit
()
db
.
commit
()
db
.
refresh
(
settings
)
db
.
refresh
(
settings
)
return
{
"allow_registration"
:
settings
.
allow_registration
}
return
{
"allow_registration"
:
settings
.
allow_registration
,
}
@
router
.
put
(
"/
registration
"
)
@
router
.
put
(
"/
settings
"
)
def
set_registration_setting
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
def
update_app_settings
(
body
:
AppSettingsBody
,
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
settings
=
db
.
query
(
AppSettings
)
.
first
()
settings
=
db
.
query
(
AppSettings
)
.
first
()
if
not
settings
:
if
not
settings
:
settings
=
AppSettings
(
allow_registration
=
True
)
settings
=
AppSettings
(
allow_registration
=
True
)
db
.
add
(
settings
)
db
.
add
(
settings
)
if
body
.
allow_registration
is
not
None
:
settings
.
allow_registration
=
body
.
allow_registration
db
.
commit
()
db
.
commit
()
db
.
refresh
(
settings
)
db
.
refresh
(
settings
)
# Toggle
return
{
settings
.
allow_registration
=
not
settings
.
allow_registration
"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
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
()
db
.
commit
()
return
{
"allow_registration"
:
settings
.
allow_registration
}
return
get_user_permissions
(
user_id
,
db
)
\ No newline at end of file
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
from
pydantic
import
BaseModel
...
@@ -28,23 +28,22 @@ class LoginBody(BaseModel):
...
@@ -28,23 +28,22 @@ class LoginBody(BaseModel):
password
:
str
password
:
str
@
router
.
get
(
"/config"
)
class
ProfileOut
(
BaseModel
):
def
auth_config
(
db
:
Session
=
Depends
(
get_db
)):
pass
"""Public endpoint — no auth needed. Returns registration status."""
settings
=
db
.
query
(
AppSettings
)
.
first
()
return
{
class
Config
(
BaseModel
):
"allow_registration"
:
settings
.
allow_registration
if
settings
else
True
,
pass
}
@
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
# Check if registration is enabled
app_
settings
=
db
.
query
(
AppSettings
)
.
first
()
settings
=
db
.
query
(
AppSettings
)
.
first
()
if
app_settings
and
not
app_
settings
.
allow_registration
:
if
settings
and
not
settings
.
allow_registration
:
raise
HTTPException
(
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
status
.
HTTP_403_FORBIDDEN
,
"Registration is currently disabled. Contact
an
administrator."
,
"Registration is currently disabled. Contact
your
administrator."
,
)
)
if
db
.
query
(
User
)
.
filter
(
if
db
.
query
(
User
)
.
filter
(
...
@@ -63,6 +62,7 @@ def register(body: RegisterBody, db: Session = Depends(get_db)):
...
@@ -63,6 +62,7 @@ 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
)
...
@@ -88,6 +88,16 @@ def me(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
...
@@ -88,6 +88,16 @@ def me(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
return
_user_dict
(
user
,
perms
)
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
:
def
_user_dict
(
u
:
User
,
perms
:
dict
=
None
)
->
dict
:
d
=
{
d
=
{
"id"
:
u
.
id
,
"id"
:
u
.
id
,
...
...
frontend/src/api.js
View file @
705bdcba
// ── Registration toggle (append to end of file) ──
const
BASE
=
"/api"
;
export
const
getAuthConfig
=
()
=>
function
headers
(
token
)
{
fetch
(
`
${
BASE
}
/auth/config`
).
then
((
r
)
=>
r
.
json
());
const
h
=
{
"Content-Type"
:
"application/json"
};
if
(
token
)
h
[
"Authorization"
]
=
`Bearer
${
token
}
`
;
return
h
;
}
export
const
getRegistrationSetting
=
(
token
)
=>
function
authHeader
(
token
)
{
request
(
"GET"
,
"/admin/registration"
,
token
);
return
token
?
{
Authorization
:
`Bearer
${
token
}
`
}
:
{};
}
export
const
toggleRegistration
=
(
token
)
=>
async
function
request
(
method
,
path
,
token
,
body
)
{
request
(
"PUT"
,
"/admin/registration"
,
token
);
const
opts
=
{
method
,
headers
:
headers
(
token
)
};
\ No newline at end of file
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
React
,
{
useState
,
useEffect
,
useCallback
}
from
"react"
;
import
{
useNavigate
}
from
"react-router-dom"
;
import
{
useApp
}
from
"../store"
;
import
{
useApp
}
from
"../store"
;
import
{
useNavigate
}
from
"react-router-dom"
;
import
{
import
{
adminStats
,
adminListUsers
,
adminCreateUser
,
adminUpdateUser
,
adminDeleteUser
,
adminStats
,
adminListUsers
,
adminCreateUser
,
adminUpdateUser
,
adminGetUserPermissions
,
adminUpdateUserPermissions
,
adminDeleteUser
,
adminListChats
,
adminGetSettings
,
adminUpdateSettings
,
adminGetDefaultPermissions
,
adminUpdateDefaultPermissions
,
adminApplyDefaults
,
}
from
"../api"
;
}
from
"../api"
;
import
{
import
{
ArrowLeft
,
Users
,
MessageSquare
,
Database
,
Zap
,
ArrowLeft
,
Users
,
MessageSquare
,
Database
,
Zap
,
Plus
,
Trash2
,
Edit
,
UserPlus
,
Trash2
,
Shield
,
ShieldOff
,
Save
,
X
,
Shield
,
ShieldCheck
,
ShieldX
,
RefreshCw
,
ToggleLeft
,
ToggleRight
,
Settings2
,
Check
,
Globe
,
Paintbrush
,
BookOpen
,
GitBranch
,
Settings
,
UserPlus
,
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
navigate
=
useNavigate
();
const
[
stats
,
setStats
]
=
useState
(
null
);
const
[
stats
,
setStats
]
=
useState
(
null
);
const
[
users
,
setUsers
]
=
useState
([]);
const
[
users
,
setUsers
]
=
useState
([]);
const
[
chats
,
setChats
]
=
useState
([]);
const
[
tab
,
setTab
]
=
useState
(
"stats"
);
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
[
editingUser
,
setEditingUser
]
=
useState
(
null
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
appSettings
,
setAppSettings
]
=
useState
({
allow_registration
:
true
});
// Permissions modal
const
[
settingsLoading
,
setSettingsLoading
]
=
useState
(
false
);
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
load
=
useCallback
(
async
()
=>
{
const
load
=
useCallback
(
async
()
=>
{
try
{
try
{
const
[
s
,
u
]
=
await
Promise
.
all
([
adminStats
(
state
.
token
),
adminListUsers
(
state
.
token
)]);
const
[
s
,
u
,
c
]
=
await
Promise
.
all
([
setStats
(
s
);
setUsers
(
u
);
adminStats
(
state
.
token
),
}
catch
(
err
)
{
setError
(
err
.
message
);
}
adminListUsers
(
state
.
token
),
},
[
state
.
token
]);
adminListChats
(
state
.
token
),
]);
useEffect
(()
=>
{
load
();
},
[
load
]
);
setStats
(
s
);
setUsers
(
u
);
async
function
handleCreate
(
e
)
{
setChats
(
c
);
e
.
preventDefault
();
}
catch
(
err
)
{
try
{
await
adminCreateUser
(
state
.
token
,
newUser
);
setShowCreate
(
false
);
setNewUser
({
username
:
""
,
email
:
""
,
password
:
""
,
role
:
"user"
,
quota_tokens_monthly
:
2000000
});
load
();
}
catch
(
err
)
{
setError
(
err
.
message
);
}
setError
(
err
.
message
);
}
}
},
[
state
.
token
]);
async
function
handleSaveEdit
(
userId
)
{
const
loadSettings
=
useCallback
(
async
()
=>
{
try
{
await
adminUpdateUser
(
state
.
token
,
userId
,
editData
);
setEditId
(
null
);
load
();
}
catch
(
err
)
{
setError
(
err
.
message
);
}
try
{
}
const
s
=
await
adminGetSettings
(
state
.
token
);
setAppSettings
(
s
);
}
catch
{
}
},
[
state
.
token
]);
async
function
handleDelete
(
userId
,
username
)
{
useEffect
(()
=>
{
load
();
loadSettings
();
},
[
load
,
loadSettings
]);
if
(
!
confirm
(
`Delete user "
${
username
}
"? This is permanent.`
))
return
;
try
{
await
adminDeleteUser
(
state
.
token
,
userId
);
load
();
}
catch
(
err
)
{
setError
(
err
.
message
);
}
}
// ═══ Permissions ═══
async
function
handleCreateUser
()
{
async
function
openPerms
(
user
)
{
setPermsUser
(
user
);
setPermsLoading
(
true
);
setPermsData
(
null
);
try
{
try
{
const
data
=
user
.
id
===
"__defaults__"
await
adminCreateUser
(
state
.
token
,
newUser
);
?
await
adminGetDefaultPermissions
(
state
.
token
)
setShowCreate
(
false
);
:
await
adminGetUserPermissions
(
state
.
token
,
user
.
id
);
setNewUser
({
username
:
""
,
email
:
""
,
password
:
""
,
role
:
"user"
,
quota_tokens_monthly
:
2000000
});
setPermsData
(
data
);
load
();
}
catch
(
err
)
{
setError
(
err
.
message
);
setPermsUser
(
null
);
}
}
catch
(
err
)
{
setError
(
err
.
message
);
}
setPermsLoading
(
false
);
}
}
async
function
savePerms
()
{
async
function
handleUpdateUser
(
userId
,
data
)
{
if
(
!
permsUser
||
!
permsData
)
return
;
setPermsSaving
(
true
);
try
{
try
{
if
(
permsUser
.
id
===
"__defaults__"
)
{
await
adminUpdateUser
(
state
.
token
,
userId
,
data
);
await
adminUpdateDefaultPermissions
(
state
.
token
,
permsData
);
setEditingUser
(
null
);
}
else
{
load
();
await
adminUpdateUserPermissions
(
state
.
token
,
permsUser
.
id
,
permsData
);
}
setPermsUser
(
null
);
setPermsData
(
null
);
}
catch
(
err
)
{
setError
(
err
.
message
);
}
}
catch
(
err
)
{
setError
(
err
.
message
);
}
setPermsSaving
(
false
);
}
}
async
function
handleApplyDefaults
()
{
async
function
handleDeleteUser
(
userId
)
{
if
(
!
confirm
(
"Apply default permissions to ALL regular users? This will overwrite their current permissions."
))
return
;
if
(
!
confirm
(
"Delete this user and all their data?"
))
return
;
setApplyingDefaults
(
true
);
try
{
try
{
const
res
=
await
adminApplyDefaults
(
state
.
token
);
await
adminDeleteUser
(
state
.
token
,
userId
);
alert
(
`✅ Applied to
${
res
.
users_updated
}
users`
);
load
(
);
}
catch
(
err
)
{
setError
(
err
.
message
);
}
}
catch
(
err
)
{
setError
(
err
.
message
);
}
setApplyingDefaults
(
false
);
}
function
updatePerm
(
key
,
value
)
{
setPermsData
(
prev
=>
({
...
prev
,
[
key
]:
value
}));
}
}
function
formatNum
(
n
)
{
async
function
toggleRegistration
()
{
if
(
n
>=
1
_000_000
)
return
(
n
/
1
_000_000
).
toFixed
(
1
)
+
"M"
;
setSettingsLoading
(
true
);
if
(
n
>=
1
_000
)
return
(
n
/
1
_000
).
toFixed
(
0
)
+
"K"
;
try
{
return
String
(
n
);
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"
)
{
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
(
return
(
<
div
className=
"h-full overflow-y-auto bg-anton-bg p-4 sm:p-6"
>
<
div
className=
"h-dvh flex flex-col bg-anton-bg"
>
<
div
className=
"max-w-6xl mx-auto space-y-6 animate-fade-in"
>
{
/* Header */
}
{
/* Header */
}
<
div
className=
"flex items-center gap-4
"
>
<
div
className=
"border-b border-anton-border bg-anton-surface px-4 py-3 flex items-center gap-3
"
>
<
button
onClick=
{
()
=>
navigate
(
"/"
)
}
className=
"p-2 rounded-lg bg-anton-surface border border-anton-border hover:border-anton-accent transition
"
><
ArrowLeft
size=
{
20
}
/></
button
>
<
button
onClick=
{
()
=>
navigate
(
"/"
)
}
className=
"text-anton-muted hover:text-white
"
><
ArrowLeft
size=
{
20
}
/></
button
>
<
div
>
<
ShieldCheck
size=
{
20
}
className=
"text-anton-accent"
/
>
<
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-white font-semibold"
>
Admin Dashboard
</
h1
>
<
p
className=
"text-anton-muted text-sm"
>
Users, permissions
&
cost control
</
p
>
<
button
onClick=
{
()
=>
{
load
();
loadSettings
();
}
}
className=
"ml-auto text-anton-muted hover:text-white"
><
RefreshCw
size=
{
16
}
/></
button
>
</
div
>
</
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
>
</
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 */
}
{
/* Content */
}
{
stats
&&
(
<
div
className=
"flex-1 overflow-y-auto p-4"
>
<
div
className=
"grid grid-cols-2 md:grid-cols-4 gap-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
:
"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
:
ShieldCheck
,
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-yellow-400"
},
].
map
((
s
)
=>
(
{
label
:
"Tokens Used"
,
value
:
(
stats
.
total_tokens_used
||
0
).
toLocaleString
(),
icon
:
Zap
,
color
:
"text-red-400"
},
<
div
key=
{
s
.
label
}
className=
"bg-anton-surface border border-anton-border rounded-xl p-4"
>
{
label
:
"Knowledge Bases"
,
value
:
stats
.
total_knowledge_bases
,
icon
:
Database
,
color
:
"text-cyan-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=
{
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
>
))
}
))
}
</
div
>
</
div
>
)
}
)
}
{
/* User Management */
}
{
/* ── SETTINGS TAB ── */
}
<
div
className=
"bg-anton-surface border border-anton-border rounded-xl overflow-hidden"
>
{
tab
===
"settings"
&&
(
<
div
className=
"px-5 py-4 border-b border-anton-border flex items-center justify-between flex-wrap gap-2"
>
<
div
className=
"space-y-4 max-w-lg"
>
<
h2
className=
"text-lg font-semibold text-white"
>
Users
</
h2
>
<
h2
className=
"text-lg font-semibold text-white flex items-center gap-2"
>
<
div
className=
"flex items-center gap-2"
>
<
Settings
size=
{
18
}
className=
"text-anton-accent"
/>
App Settings
<
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"
>
</
h2
>
<
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
>
)
}
<
div
className=
"overflow-x-auto"
>
<
div
className=
"bg-anton-card border border-anton-border rounded-xl p-5"
>
<
table
className=
"w-full text-sm"
>
<
div
className=
"flex items-center justify-between"
>
<
thead
><
tr
className=
"text-left text-anton-muted border-b border-anton-border"
>
<
div
>
<
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
>
<
h3
className=
"text-white font-medium flex items-center gap-2"
>
</
tr
></
thead
>
<
UserPlus
size=
{
16
}
className=
"text-blue-400"
/>
<
tbody
>
User Registration
{
users
.
map
((
u
)
=>
(
</
h3
>
<
tr
key=
{
u
.
id
}
className=
"border-b border-anton-border/50 hover:bg-anton-card/50 transition"
>
<
p
className=
"text-anton-muted text-sm mt-1"
>
<
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
>
{
appSettings
.
allow_registration
<
td
className=
"px-5 py-3"
>
?
"Anyone can create an account on the login page."
{
editId
===
u
.
id
?
(
:
"Registration is disabled. Only admins can create users."
}
<
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"
>
</
p
>
<
option
value=
"user"
>
user
</
option
><
option
value=
"admin"
>
admin
</
option
><
option
value=
"superadmin"
>
superadmin
</
option
>
</
div
>
</
select
>
<
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"
/>
)
:
(
)
:
(
<
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
>
<
ToggleLeft
size=
{
40
}
className=
"text-anton-muted"
/
>
)
}
)
}
</
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
>
</
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
>
</
div
>
</
div
>
</
div
>
)
}
{
/* ═══ PERMISSIONS MODAL ═══ */
}
{
/* ── USERS TAB ── */
}
{
permsUser
&&
(
{
tab
===
"users"
&&
(
<
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=
"space-y-3"
>
<
div
className=
"bg-anton-surface border border-anton-border rounded-2xl w-full max-w-2xl my-8 shadow-2xl"
onClick=
{
e
=>
e
.
stopPropagation
()
}
>
<
div
className=
"flex items-center justify-between"
>
{
/* Modal Header */
}
<
h2
className=
"text-lg font-semibold text-white"
>
Users (
{
users
.
length
}
)
</
h2
>
<
div
className=
"px-6 py-4 border-b border-anton-border flex items-center justify-between"
>
<
button
onClick=
{
()
=>
setShowCreate
(
!
showCreate
)
}
<
div
>
className=
"flex items-center gap-1.5 bg-anton-accent text-white px-3 py-1.5 rounded-lg text-sm hover:opacity-80"
>
<
h2
className=
"text-lg font-bold text-white flex items-center gap-2"
>
<
Plus
size=
{
14
}
/>
Create User
<
Settings2
size=
{
20
}
className=
"text-purple-400"
/>
</
button
>
{
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
>
</
div
>
{
permsLoading
?
(
{
showCreate
&&
(
<
div
className=
"flex items-center justify-center py-20"
><
Loader2
size=
{
24
}
className=
"text-anton-accent animate-spin"
/></
div
>
<
div
className=
"bg-anton-card border border-anton-border rounded-xl p-4 space-y-3 animate-fade-in"
>
)
:
permsData
?
(
<
h3
className=
"text-sm font-semibold text-white"
>
New User
</
h3
>
<
div
className=
"p-6 space-y-6 max-h-[70vh] overflow-y-auto"
>
<
div
className=
"grid grid-cols-2 gap-3"
>
{
/* Feature Access */
}
<
input
placeholder=
"Username"
value=
{
newUser
.
username
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
username
:
e
.
target
.
value
})
}
<
div
>
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"
/>
<
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
>
<
input
placeholder=
"Email"
value=
{
newUser
.
email
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
email
:
e
.
target
.
value
})
}
<
div
className=
"space-y-2"
>
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"
/>
{
FEATURE_DEFS
.
map
(
f
=>
{
<
input
placeholder=
"Password"
type=
"password"
value=
{
newUser
.
password
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
password
:
e
.
target
.
value
})
}
const
Icon
=
f
.
icon
;
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"
/>
return
(
<
select
value=
{
newUser
.
role
}
onChange=
{
(
e
)
=>
setNewUser
({
...
newUser
,
role
:
e
.
target
.
value
})
}
<
div
key=
{
f
.
key
}
className=
"flex items-center justify-between bg-anton-card rounded-lg px-4 py-3 border border-anton-border"
>
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=
"flex items-center gap-3"
>
<
option
value=
"user"
>
User
</
option
>
<
Icon
size=
{
16
}
className=
{
f
.
color
}
/>
<
option
value=
"admin"
>
Admin
</
option
>
<
div
>
</
select
>
<
p
className=
"text-sm text-white font-medium"
>
{
f
.
label
}
</
p
>
<
p
className=
"text-[10px] text-anton-muted"
>
{
f
.
desc
}
</
p
>
</
div
>
</
div
>
</
div
>
<
Toggle
value=
{
!!
permsData
[
f
.
key
]
}
onChange=
{
v
=>
updatePerm
(
f
.
key
,
v
)
}
/>
<
div
className=
"flex gap-2"
>
</
div
>
<
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
>
</
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"
>
<
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"
>
{
users
.
map
((
u
)
=>
(
<
input
type=
"checkbox"
checked=
{
permsData
.
allowed_models
===
"all"
}
onChange=
{
e
=>
updatePerm
(
"allowed_models"
,
e
.
target
.
checked
?
"all"
:
MODELS
.
map
(
m
=>
m
.
id
).
join
(
","
))
}
<
div
key=
{
u
.
id
}
className=
"bg-anton-card border border-anton-border rounded-xl p-4 flex items-center gap-4"
>
className=
"w-4 h-4 rounded accent-anton-accent"
/>
<
div
className=
"flex-1 min-w-0"
>
<
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
>
<
div
className=
"flex items-center gap-2"
>
</
label
>
<
span
className=
"text-white font-medium"
>
{
u
.
username
}
</
span
>
{
permsData
.
allowed_models
!==
"all"
&&
(
<
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"}`
}
>
<
div
className=
"pl-4 space-y-1.5"
>
{
u
.
role
}
{
MODELS
.
map
(
m
=>
{
</
span
>
const
list
=
(
permsData
.
allowed_models
||
""
).
split
(
","
).
map
(
s
=>
s
.
trim
()).
filter
(
Boolean
);
{
!
u
.
is_active
&&
<
span
className=
"text-[10px] px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400"
>
disabled
</
span
>
}
const
checked
=
list
.
includes
(
m
.
id
);
return
(
<
label
key=
{
m
.
id
}
className=
"flex items-center gap-3 bg-anton-bg rounded-lg px-3 py-2 border border-anton-border/50 cursor-pointer"
>
<
input
type=
"checkbox"
checked=
{
checked
}
onChange=
{
e
=>
{
let
next
=
checked
?
list
.
filter
(
x
=>
x
!==
m
.
id
)
:
[...
list
,
m
.
id
];
if
(
!
next
.
length
)
next
=
[
m
.
id
];
// Can't have zero models
updatePerm
(
"allowed_models"
,
next
.
join
(
","
));
}
}
className=
"w-4 h-4 rounded accent-anton-accent"
/>
<
div
><
span
className=
"text-sm text-white"
>
{
m
.
label
}
</
span
><
span
className=
"text-[10px] text-anton-muted ml-2"
>
{
m
.
tier
}
</
span
></
div
>
</
label
>
);
})
}
</
div
>
)
}
</
div
>
</
div
>
<
div
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
>
<
div
className=
"flex items-center gap-1"
>
{
/* Limits */
}
{
u
.
role
!==
"superadmin"
&&
(
<
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
>
<
button
onClick=
{
()
=>
handleUpdateUser
(
u
.
id
,
{
is_active
:
!
u
.
is_active
})
}
<
div
className=
"grid grid-cols-1 sm:grid-cols-2 gap-3"
>
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"}`
}
{
LIMIT_DEFS
.
map
(
l
=>
(
title=
{
u
.
is_active
?
"Disable"
:
"Enable"
}
>
<
div
key=
{
l
.
key
}
className=
"bg-anton-card rounded-lg px-4 py-3 border border-anton-border"
>
{
u
.
is_active
?
<
ShieldCheck
size=
{
16
}
/>
:
<
ShieldX
size=
{
16
}
/>
}
<
div
className=
"flex justify-between items-center mb-1"
>
</
button
>
<
label
className=
"text-xs text-anton-muted"
>
{
l
.
label
}
</
label
>
<
button
onClick=
{
()
=>
handleDeleteUser
(
u
.
id
)
}
className=
"p-1.5 rounded-lg text-red-400 hover:bg-red-500/10 transition"
title=
"Delete"
>
<
span
className=
"text-xs text-white font-mono"
>
{
(
permsData
[
l
.
key
]
||
0
)
===
0
&&
l
.
desc
?.
includes
(
"unlimited"
)
?
"∞"
:
(
permsData
[
l
.
key
]
||
0
).
toLocaleString
()
}
</
span
>
<
Trash2
size=
{
16
}
/>
</
button
>
</>
)
}
</
div
>
</
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
>
))
}
))
}
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
)
}
)
:
null
}
{
/* Modal Footer */
}
{
/* ── CHATS TAB ── */
}
{
permsData
&&
(
{
tab
===
"chats"
&&
(
<
div
className=
"px-6 py-4 border-t border-anton-border flex items-center justify-between"
>
<
div
className=
"space-y-2"
>
<
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
>
<
h2
className=
"text-lg font-semibold text-white"
>
Recent Chats (
{
chats
.
length
}
)
</
h2
>
<
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"
>
{
chats
.
map
((
c
)
=>
(
{
permsSaving
?
<
Loader2
size=
{
14
}
className=
"animate-spin"
/>
:
<
Save
size=
{
14
}
/>
}
Save Permissions
<
div
key=
{
c
.
id
}
className=
"bg-anton-card border border-anton-border rounded-xl p-3 flex items-center gap-3"
>
</
button
>
<
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
>
)
}
</
div
>
</
div
>
))
}
</
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
React
,
{
useState
,
useEffect
}
from
"react"
;
import
{
useApp
}
from
"../store"
;
import
{
useApp
}
from
"../store"
;
import
{
login
,
register
,
get
Auth
Config
}
from
"../api"
;
import
{
login
,
register
,
get
Public
Config
}
from
"../api"
;
import
{
Flame
,
LogIn
,
UserPlus
,
AlertCircle
}
from
"lucide-react"
;
import
{
Flame
,
Eye
,
EyeOff
,
Loader2
}
from
"lucide-react"
;
export
default
function
LoginPage
()
{
export
default
function
LoginPage
()
{
const
{
dispatch
}
=
useApp
();
const
{
dispatch
}
=
useApp
();
...
@@ -9,21 +9,23 @@ export default function LoginPage() {
...
@@ -9,21 +9,23 @@ export default function LoginPage() {
const
[
username
,
setUsername
]
=
useState
(
""
);
const
[
username
,
setUsername
]
=
useState
(
""
);
const
[
email
,
setEmail
]
=
useState
(
""
);
const
[
email
,
setEmail
]
=
useState
(
""
);
const
[
password
,
setPassword
]
=
useState
(
""
);
const
[
password
,
setPassword
]
=
useState
(
""
);
const
[
showPw
,
setShowPw
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
allowRegistration
,
setAllowRegistration
]
=
useState
(
true
);
const
[
allowRegistration
,
setAllowRegistration
]
=
useState
(
true
);
const
[
configLoaded
,
setConfigLoaded
]
=
useState
(
false
);
const
[
configLoaded
,
setConfigLoaded
]
=
useState
(
false
);
useEffect
(()
=>
{
useEffect
(()
=>
{
getAuthConfig
()
(
async
()
=>
{
.
then
((
data
)
=>
{
try
{
setAllowRegistration
(
data
.
allow_registration
!==
false
);
const
cfg
=
await
getPublicConfig
(
);
set
ConfigLoaded
(
true
);
set
AllowRegistration
(
cfg
.
allow_registration
);
}
)
}
catch
{
.
catch
(()
=>
{
// If config fetch fails, default to allowing registration
setAllowRegistration
(
true
);
setAllowRegistration
(
true
);
}
setConfigLoaded
(
true
);
setConfigLoaded
(
true
);
}
);
})(
);
},
[]);
},
[]);
async
function
handleSubmit
(
e
)
{
async
function
handleSubmit
(
e
)
{
...
@@ -31,26 +33,31 @@ export default function LoginPage() {
...
@@ -31,26 +33,31 @@ export default function LoginPage() {
setError
(
""
);
setError
(
""
);
setLoading
(
true
);
setLoading
(
true
);
try
{
try
{
let
res
ult
;
let
res
;
if
(
isRegister
)
{
if
(
isRegister
)
{
if
(
!
allowRegistration
)
{
if
(
!
email
)
{
setError
(
"Email is required"
);
setLoading
(
false
);
return
;
}
setError
(
"Registration is disabled."
);
res
=
await
register
(
username
,
email
,
password
);
setLoading
(
false
);
return
;
}
result
=
await
register
(
username
,
email
,
password
);
}
else
{
}
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
)
{
}
catch
(
err
)
{
setError
(
err
.
message
||
"Something went wrong"
);
setError
(
err
.
message
);
}
}
setLoading
(
false
);
setLoading
(
false
);
}
}
if
(
!
configLoaded
)
{
return
(
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"
>
<
Loader2
className=
"animate-spin text-anton-accent"
size=
{
32
}
/>
</
div
>
);
}
return
(
<
div
className=
"h-dvh flex items-center justify-center bg-anton-bg px-4"
>
<
div
className=
"w-full max-w-sm"
>
<
div
className=
"w-full max-w-sm"
>
<
div
className=
"flex flex-col items-center mb-8"
>
<
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"
>
<
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() {
...
@@ -60,85 +67,65 @@ export default function LoginPage() {
<
p
className=
"text-anton-muted text-sm mt-1"
>
Avatar of All Elements of Code
</
p
>
<
p
className=
"text-anton-muted text-sm mt-1"
>
Avatar of All Elements of Code
</
p
>
</
div
>
</
div
>
<
div
className=
"bg-anton-card border border-anton-border rounded-2xl p-6"
>
<
form
onSubmit=
{
handleSubmit
}
className=
"bg-anton-card border border-anton-border rounded-2xl p-6 space-y-4"
>
{
/* Tab buttons — only show Register tab if registration is allowed */
}
<
h2
className=
"text-lg font-semibold text-white text-center"
>
<
div
className=
"flex gap-2 mb-6"
>
{
isRegister
?
"Create Account"
:
"Sign In"
}
<
button
</
h2
>
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
>
{
error
&&
(
{
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"
>
<
div
className=
"bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 text-red-400 text-sm"
>
<
AlertCircle
size=
{
14
}
className=
"text-red-400 mt-0.5 shrink-0"
/>
{
error
}
<
span
className=
"text-red-400 text-xs"
>
{
error
}
</
span
>
</
div
>
</
div
>
)
}
)
}
<
form
onSubmit=
{
handleSubmit
}
className=
"space-y-4"
>
<
div
>
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Username
</
label
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Username
</
label
>
<
input
<
input
type=
"text"
value=
{
username
}
onChange=
{
(
e
)
=>
setUsername
(
e
.
target
.
value
)
}
required
autoFocus
type=
"text"
value=
{
username
}
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"
/>
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"
/>
</
div
>
</
div
>
{
isRegister
&&
(
{
isRegister
&&
(
<
div
>
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Email
</
label
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Email
</
label
>
<
input
<
input
type=
"email"
value=
{
email
}
onChange=
{
(
e
)
=>
setEmail
(
e
.
target
.
value
)
}
required
type=
"email"
value=
{
email
}
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"
/>
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
>
)
}
)
}
<
div
>
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Password
</
label
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Password
</
label
>
<
input
<
div
className=
"relative"
>
type=
"password"
value=
{
password
}
<
input
type=
{
showPw
?
"text"
:
"password"
}
value=
{
password
}
onChange=
{
(
e
)
=>
setPassword
(
e
.
target
.
value
)
}
required
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 pr-10"
/>
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"
<
button
type=
"button"
onClick=
{
()
=>
setShowPw
(
!
showPw
)
}
required
autoComplete=
{
isRegister
?
"new-password"
:
"current-password"
}
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
>
</
div
>
<
button
<
button
type=
"submit"
disabled=
{
loading
}
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"
>
className=
"w-full bg-anton-accent text-white py-2.5 rounded-lg font-medium hover:opacity-90 transition disabled:opacity-50"
{
loading
&&
<
Loader2
size=
{
16
}
className=
"animate-spin"
/>
}
>
{
isRegister
?
"Create Account"
:
"Sign In"
}
{
loading
?
"..."
:
isRegister
?
"Create Account"
:
"Sign In"
}
</
button
>
</
button
>
</
form
>
{
!
allowRegistration
&&
configLoaded
&&
!
isRegister
&&
(
{
allowRegistration
&&
(
<
p
className=
"text-[11px] text-anton-muted text-center mt-4"
>
<
p
className=
"text-center text-sm text-anton-muted"
>
Registration is currently disabled by the administrator.
{
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
>
)
}
{
!
allowRegistration
&&
!
isRegister
&&
(
<
p
className=
"text-center text-xs text-anton-muted"
>
Registration is disabled. Contact your administrator.
</
p
>
</
p
>
)
}
)
}
</
div
>
</
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