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
6b9a6730
Commit
6b9a6730
authored
Mar 29, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
topc
parent
da697713
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
0 additions
and
454 deletions
+0
-454
admin_routes.py
backend/routes/admin_routes.py
+0
-130
attachment_routes_15.py
backend/routes/attachment_routes_15.py
+0
-159
attachment_routes_16.py
backend/routes/attachment_routes_16.py
+0
-165
No files found.
backend/routes/admin_routes.py
deleted
100644 → 0
View file @
da697713
"""
Superadmin routes: user management, stats, oversight.
"""
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
from
backend.auth
import
require_superadmin
,
hash_password
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
@
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
()
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
\ No newline at end of file
backend/routes/attachment_routes_15.py
deleted
100644 → 0
View file @
da697713
"""
Chat attachment upload, serve, and delete routes.
"""
import
os
from
typing
import
Optional
from
fastapi
import
APIRouter
,
Depends
,
HTTPException
,
UploadFile
,
File
,
Query
from
fastapi.responses
import
FileResponse
from
sqlalchemy.orm
import
Session
from
backend.database
import
get_db
from
backend.models
import
User
,
Chat
,
ChatAttachment
from
backend.auth
import
get_current_user
,
decode_token
from
backend.services
import
attachment_service
from
backend.config
import
MAX_ATTACHMENT_BYTES
router
=
APIRouter
()
@
router
.
post
(
"/chats/{chat_id}/attachments"
)
async
def
upload_attachments
(
chat_id
:
str
,
files
:
list
[
UploadFile
]
=
File
(
...
),
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Upload one or more files as chat attachments. Returns attachment metadata."""
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
raise
HTTPException
(
404
,
"Chat not found"
)
results
=
[]
for
file
in
files
:
filename
=
file
.
filename
or
"file"
try
:
content
=
await
file
.
read
()
if
len
(
content
)
>
MAX_ATTACHMENT_BYTES
:
results
.
append
({
"error"
:
f
"File too large: {filename} ({len(content) // 1024 // 1024}MB). Max {MAX_ATTACHMENT_BYTES // 1024 // 1024}MB."
,
})
continue
meta
=
attachment_service
.
save_attachment
(
chat_id
=
chat_id
,
filename
=
filename
,
content
=
content
,
content_type
=
file
.
content_type
,
)
att
=
ChatAttachment
(
id
=
meta
[
"id"
],
chat_id
=
chat_id
,
filename
=
meta
[
"filename"
],
original_filename
=
meta
[
"original_filename"
],
mime_type
=
meta
[
"mime_type"
],
file_type
=
meta
[
"file_type"
],
file_size
=
meta
[
"file_size"
],
storage_path
=
meta
[
"storage_path"
],
text_extract
=
meta
.
get
(
"text_extract"
),
)
db
.
add
(
att
)
db
.
commit
()
db
.
refresh
(
att
)
results
.
append
(
_att_dict
(
att
))
except
Exception
as
e
:
results
.
append
({
"error"
:
f
"Failed to upload {filename}: {str(e)}"
})
return
{
"attachments"
:
results
}
@
router
.
get
(
"/attachments/{attachment_id}/file"
)
def
serve_attachment
(
attachment_id
:
str
,
token
:
Optional
[
str
]
=
Query
(
None
),
user
:
Optional
[
User
]
=
Depends
(
_optional_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""
Serve an attachment file.
Supports both Bearer header auth and ?token= query param
(needed for <img> tags that can't send headers).
"""
# Try query param auth if header auth didn't work
if
user
is
None
and
token
:
try
:
payload
=
decode_token
(
token
)
user
=
db
.
query
(
User
)
.
filter
(
User
.
id
==
payload
[
"sub"
])
.
first
()
except
Exception
:
pass
if
user
is
None
:
raise
HTTPException
(
401
,
"Authentication required"
)
att
=
db
.
query
(
ChatAttachment
)
.
filter
(
ChatAttachment
.
id
==
attachment_id
)
.
first
()
if
not
att
:
raise
HTTPException
(
404
,
"Attachment not found"
)
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
att
.
chat_id
)
.
first
()
if
not
chat
or
(
chat
.
user_id
!=
user
.
id
and
user
.
role
!=
"superadmin"
):
raise
HTTPException
(
403
,
"Access denied"
)
if
not
os
.
path
.
exists
(
att
.
storage_path
):
raise
HTTPException
(
404
,
"File not found on disk"
)
return
FileResponse
(
att
.
storage_path
,
media_type
=
att
.
mime_type
,
filename
=
att
.
original_filename
,
)
@
router
.
delete
(
"/attachments/{attachment_id}"
)
def
delete_attachment
(
attachment_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Delete a single attachment."""
att
=
db
.
query
(
ChatAttachment
)
.
filter
(
ChatAttachment
.
id
==
attachment_id
)
.
first
()
if
not
att
:
raise
HTTPException
(
404
)
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
att
.
chat_id
)
.
first
()
if
not
chat
or
(
chat
.
user_id
!=
user
.
id
and
user
.
role
!=
"superadmin"
):
raise
HTTPException
(
403
)
attachment_service
.
delete_attachment_file
(
att
.
storage_path
)
db
.
delete
(
att
)
db
.
commit
()
return
{
"ok"
:
True
}
def
_optional_current_user
(
db
:
Session
=
Depends
(
get_db
),
):
"""
A dependency that tries to get current user but returns None on failure.
This allows the endpoint to also accept ?token= query param.
"""
# This is a placeholder — the actual auth is handled in the route
# by checking both header and query param
return
None
def
_att_dict
(
att
:
ChatAttachment
)
->
dict
:
return
{
"id"
:
att
.
id
,
"chat_id"
:
att
.
chat_id
,
"message_id"
:
att
.
message_id
,
"filename"
:
att
.
filename
,
"original_filename"
:
att
.
original_filename
,
"mime_type"
:
att
.
mime_type
,
"file_type"
:
att
.
file_type
,
"file_size"
:
att
.
file_size
,
"created_at"
:
str
(
att
.
created_at
),
}
\ No newline at end of file
backend/routes/attachment_routes_16.py
deleted
100644 → 0
View file @
da697713
"""
Chat attachment upload, serve, and delete routes.
"""
import
os
from
typing
import
Optional
from
fastapi
import
APIRouter
,
Depends
,
HTTPException
,
UploadFile
,
File
,
Query
,
Request
from
fastapi.responses
import
FileResponse
from
sqlalchemy.orm
import
Session
from
backend.database
import
get_db
from
backend.models
import
User
,
Chat
,
ChatAttachment
from
backend.auth
import
get_current_user
,
decode_token
from
backend.services
import
attachment_service
from
backend.config
import
MAX_ATTACHMENT_BYTES
router
=
APIRouter
()
def
_get_user_from_request
(
request
:
Request
,
db
:
Session
,
token_param
:
Optional
[
str
]
=
None
)
->
User
:
"""
Resolve user from either:
1. Authorization: Bearer <token> header
2. ?token=<token> query parameter (for img/video tags)
"""
raw_token
=
None
# Try header first
auth_header
=
request
.
headers
.
get
(
"authorization"
,
""
)
if
auth_header
.
startswith
(
"Bearer "
):
raw_token
=
auth_header
[
7
:]
# Fall back to query param
if
not
raw_token
and
token_param
:
raw_token
=
token_param
if
not
raw_token
:
raise
HTTPException
(
401
,
"Authentication required"
)
payload
=
decode_token
(
raw_token
)
user
=
db
.
query
(
User
)
.
filter
(
User
.
id
==
payload
[
"sub"
])
.
first
()
if
not
user
or
not
user
.
is_active
:
raise
HTTPException
(
401
,
"User not found or inactive"
)
return
user
@
router
.
post
(
"/chats/{chat_id}/attachments"
)
async
def
upload_attachments
(
chat_id
:
str
,
files
:
list
[
UploadFile
]
=
File
(
...
),
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Upload one or more files as chat attachments. Returns attachment metadata."""
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
raise
HTTPException
(
404
,
"Chat not found"
)
results
=
[]
for
file
in
files
:
filename
=
file
.
filename
or
"file"
try
:
content
=
await
file
.
read
()
if
len
(
content
)
>
MAX_ATTACHMENT_BYTES
:
results
.
append
({
"error"
:
f
"File too large: {filename} ({len(content) // 1024 // 1024}MB). Max {MAX_ATTACHMENT_BYTES // 1024 // 1024}MB."
,
})
continue
meta
=
attachment_service
.
save_attachment
(
chat_id
=
chat_id
,
filename
=
filename
,
content
=
content
,
content_type
=
file
.
content_type
,
)
att
=
ChatAttachment
(
id
=
meta
[
"id"
],
chat_id
=
chat_id
,
filename
=
meta
[
"filename"
],
original_filename
=
meta
[
"original_filename"
],
mime_type
=
meta
[
"mime_type"
],
file_type
=
meta
[
"file_type"
],
file_size
=
meta
[
"file_size"
],
storage_path
=
meta
[
"storage_path"
],
text_extract
=
meta
.
get
(
"text_extract"
),
)
db
.
add
(
att
)
db
.
commit
()
db
.
refresh
(
att
)
results
.
append
(
_att_dict
(
att
))
except
Exception
as
e
:
results
.
append
({
"error"
:
f
"Failed to upload {filename}: {str(e)}"
})
return
{
"attachments"
:
results
}
@
router
.
get
(
"/attachments/{attachment_id}/file"
)
def
serve_attachment
(
attachment_id
:
str
,
request
:
Request
,
token
:
Optional
[
str
]
=
Query
(
None
),
db
:
Session
=
Depends
(
get_db
),
):
"""
Serve an attachment file.
Supports both Bearer header auth and ?token= query param
(needed for <img> tags that can't send headers).
"""
user
=
_get_user_from_request
(
request
,
db
,
token
)
att
=
db
.
query
(
ChatAttachment
)
.
filter
(
ChatAttachment
.
id
==
attachment_id
)
.
first
()
if
not
att
:
raise
HTTPException
(
404
,
"Attachment not found"
)
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
att
.
chat_id
)
.
first
()
if
not
chat
or
(
chat
.
user_id
!=
user
.
id
and
user
.
role
!=
"superadmin"
):
raise
HTTPException
(
403
,
"Access denied"
)
if
not
os
.
path
.
exists
(
att
.
storage_path
):
raise
HTTPException
(
404
,
"File not found on disk"
)
return
FileResponse
(
att
.
storage_path
,
media_type
=
att
.
mime_type
,
filename
=
att
.
original_filename
,
)
@
router
.
delete
(
"/attachments/{attachment_id}"
)
def
delete_attachment
(
attachment_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Delete a single attachment."""
att
=
db
.
query
(
ChatAttachment
)
.
filter
(
ChatAttachment
.
id
==
attachment_id
)
.
first
()
if
not
att
:
raise
HTTPException
(
404
)
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
att
.
chat_id
)
.
first
()
if
not
chat
or
(
chat
.
user_id
!=
user
.
id
and
user
.
role
!=
"superadmin"
):
raise
HTTPException
(
403
)
attachment_service
.
delete_attachment_file
(
att
.
storage_path
)
db
.
delete
(
att
)
db
.
commit
()
return
{
"ok"
:
True
}
def
_att_dict
(
att
:
ChatAttachment
)
->
dict
:
return
{
"id"
:
att
.
id
,
"chat_id"
:
att
.
chat_id
,
"message_id"
:
att
.
message_id
,
"filename"
:
att
.
filename
,
"original_filename"
:
att
.
original_filename
,
"mime_type"
:
att
.
mime_type
,
"file_type"
:
att
.
file_type
,
"file_size"
:
att
.
file_size
,
"created_at"
:
str
(
att
.
created_at
),
}
\ No newline at end of file
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