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