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
738f4bcd
Commit
738f4bcd
authored
Mar 30, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 16 files via Son of Anton
parent
347bafba
Changes
16
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
1036 additions
and
638 deletions
+1036
-638
auth.py
backend/auth.py
+174
-21
config.py
backend/config.py
+88
-32
main.py
backend/main.py
+7
-1
models.py
backend/models.py
+42
-2
admin_routes.py
backend/routes/admin_routes.py
+108
-4
attachment_routes.py
backend/routes/attachment_routes.py
+23
-32
auth_routes.py
backend/routes/auth_routes.py
+20
-23
chat_routes.py
backend/routes/chat_routes.py
+75
-12
export_routes.py
backend/routes/export_routes.py
+8
-4
knowledge_routes.py
backend/routes/knowledge_routes.py
+61
-174
seed.py
backend/seed.py
+41
-8
api.js
frontend/src/api.js
+16
-42
ChatView.jsx
frontend/src/components/ChatView.jsx
+85
-68
Sidebar.jsx
frontend/src/components/Sidebar.jsx
+21
-27
AdminPage.jsx
frontend/src/pages/AdminPage.jsx
+244
-142
store.jsx
frontend/src/store.jsx
+23
-46
No files found.
backend/auth.py
View file @
738f4bcd
"""
"""
JWT authentication helpers.
Authentication helpers + permission system — Son of Anton v4.2.0
"""
"""
from
datetime
import
datetime
,
timedelta
from
datetime
import
datetime
,
timedelta
...
@@ -10,16 +10,15 @@ from fastapi import Depends, HTTPException, status
...
@@ -10,16 +10,15 @@ from fastapi import Depends, HTTPException, status
from
fastapi.security
import
HTTPBearer
,
HTTPAuthorizationCredentials
from
fastapi.security
import
HTTPBearer
,
HTTPAuthorizationCredentials
from
sqlalchemy.orm
import
Session
from
sqlalchemy.orm
import
Session
from
backend
import
config
from
backend.database
import
get_db
from
backend.database
import
get_db
from
backend.
models
import
User
from
backend.
config
import
JWT_SECRET
,
DEFAULT_PERMISSIONS
,
SUPERADMIN_PERMISSIONS
,
PERMISSION_FIELDS
pwd_ctx
=
CryptContext
(
schemes
=
[
"bcrypt"
],
deprecated
=
"auto"
)
pwd_ctx
=
CryptContext
(
schemes
=
[
"bcrypt"
],
deprecated
=
"auto"
)
security
=
HTTPBearer
()
bearer_scheme
=
HTTPBearer
()
def
hash_password
(
p
lain
:
str
)
->
str
:
def
hash_password
(
p
assword
:
str
)
->
str
:
return
pwd_ctx
.
hash
(
p
lain
)
return
pwd_ctx
.
hash
(
p
assword
)
def
verify_password
(
plain
:
str
,
hashed
:
str
)
->
bool
:
def
verify_password
(
plain
:
str
,
hashed
:
str
)
->
bool
:
...
@@ -30,14 +29,14 @@ def create_token(user_id: str, role: str) -> str:
...
@@ -30,14 +29,14 @@ def create_token(user_id: str, role: str) -> str:
payload
=
{
payload
=
{
"sub"
:
user_id
,
"sub"
:
user_id
,
"role"
:
role
,
"role"
:
role
,
"exp"
:
datetime
.
utcnow
()
+
timedelta
(
hours
=
config
.
JWT_EXPIRY_HOURS
),
"exp"
:
datetime
.
utcnow
()
+
timedelta
(
days
=
30
),
}
}
return
jwt
.
encode
(
payload
,
config
.
JWT_SECRET
,
algorithm
=
config
.
JWT_ALGORITHM
)
return
jwt
.
encode
(
payload
,
JWT_SECRET
,
algorithm
=
"HS256"
)
def
decode_token
(
token
:
str
)
->
dict
:
def
decode_token
(
token
:
str
)
->
dict
:
try
:
try
:
return
jwt
.
decode
(
token
,
config
.
JWT_SECRET
,
algorithms
=
[
config
.
JWT_ALGORITHM
])
return
jwt
.
decode
(
token
,
JWT_SECRET
,
algorithms
=
[
"HS256"
])
except
jwt
.
ExpiredSignatureError
:
except
jwt
.
ExpiredSignatureError
:
raise
HTTPException
(
status
.
HTTP_401_UNAUTHORIZED
,
"Token expired"
)
raise
HTTPException
(
status
.
HTTP_401_UNAUTHORIZED
,
"Token expired"
)
except
jwt
.
InvalidTokenError
:
except
jwt
.
InvalidTokenError
:
...
@@ -45,23 +44,177 @@ def decode_token(token: str) -> dict:
...
@@ -45,23 +44,177 @@ def decode_token(token: str) -> dict:
def
get_current_user
(
def
get_current_user
(
cred
s
:
HTTPAuthorizationCredentials
=
Depends
(
security
),
cred
entials
:
HTTPAuthorizationCredentials
=
Depends
(
bearer_scheme
),
db
:
Session
=
Depends
(
get_db
),
db
:
Session
=
Depends
(
get_db
),
)
->
User
:
):
payload
=
decode_token
(
creds
.
credentials
)
from
backend.models
import
User
payload
=
decode_token
(
credentials
.
credentials
)
user
=
db
.
query
(
User
)
.
filter
(
User
.
id
==
payload
[
"sub"
])
.
first
()
user
=
db
.
query
(
User
)
.
filter
(
User
.
id
==
payload
[
"sub"
])
.
first
()
if
not
user
or
not
user
.
is_active
:
if
not
user
:
raise
HTTPException
(
status
.
HTTP_401_UNAUTHORIZED
,
"User not found or inactive"
)
raise
HTTPException
(
status
.
HTTP_401_UNAUTHORIZED
,
"User not found"
)
if
not
user
.
is_active
:
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Account disabled"
)
return
user
return
user
def
require_admin
(
user
:
User
=
Depends
(
get_current_user
))
->
User
:
def
require_admin
(
if
user
.
role
not
in
(
"admin"
,
"superadmin"
):
credentials
:
HTTPAuthorizationCredentials
=
Depends
(
bearer_scheme
),
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Admin access required"
)
db
:
Session
=
Depends
(
get_db
),
):
from
backend.models
import
User
payload
=
decode_token
(
credentials
.
credentials
)
user
=
db
.
query
(
User
)
.
filter
(
User
.
id
==
payload
[
"sub"
])
.
first
()
if
not
user
or
user
.
role
not
in
(
"admin"
,
"superadmin"
):
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Admin required"
)
return
user
def
require_superadmin
(
credentials
:
HTTPAuthorizationCredentials
=
Depends
(
bearer_scheme
),
db
:
Session
=
Depends
(
get_db
),
):
from
backend.models
import
User
payload
=
decode_token
(
credentials
.
credentials
)
user
=
db
.
query
(
User
)
.
filter
(
User
.
id
==
payload
[
"sub"
])
.
first
()
if
not
user
or
user
.
role
!=
"superadmin"
:
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Superadmin required"
)
return
user
return
user
def
require_superadmin
(
user
:
User
=
Depends
(
get_current_user
))
->
User
:
# ═══════════════════════════════════════════════════
if
user
.
role
!=
"superadmin"
:
# PERMISSION SYSTEM
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Superadmin access required"
)
# ═══════════════════════════════════════════════════
return
user
\ No newline at end of file
def
get_user_permissions
(
user_id
:
str
,
db
:
Session
)
->
dict
:
"""
Get permissions dict for a user.
- Superadmins always get full permissions (bypass everything).
- Regular users get their stored permissions or defaults.
- Auto-creates permission row if missing.
"""
from
backend.models
import
User
,
UserPermissions
user
=
db
.
query
(
User
)
.
filter
(
User
.
id
==
user_id
)
.
first
()
if
not
user
:
return
dict
(
DEFAULT_PERMISSIONS
)
if
user
.
role
==
"superadmin"
:
return
dict
(
SUPERADMIN_PERMISSIONS
)
perms
=
db
.
query
(
UserPermissions
)
.
filter
(
UserPermissions
.
user_id
==
user_id
)
.
first
()
if
not
perms
:
perms
=
_create_default_permissions
(
user_id
,
db
)
return
_perms_to_dict
(
perms
)
def
get_default_permissions_template
(
db
:
Session
)
->
dict
:
"""Get the stored default permissions template, or config defaults."""
from
backend.models
import
UserPermissions
template
=
db
.
query
(
UserPermissions
)
.
filter
(
UserPermissions
.
user_id
==
"__defaults__"
)
.
first
()
if
template
:
return
_perms_to_dict
(
template
)
return
dict
(
DEFAULT_PERMISSIONS
)
def
ensure_user_permissions
(
user_id
:
str
,
db
:
Session
):
"""Make sure a user has a permissions row. Creates one from template if missing."""
from
backend.models
import
UserPermissions
existing
=
db
.
query
(
UserPermissions
)
.
filter
(
UserPermissions
.
user_id
==
user_id
)
.
first
()
if
not
existing
:
_create_default_permissions
(
user_id
,
db
)
def
_create_default_permissions
(
user_id
:
str
,
db
:
Session
):
"""Create a permissions row using the stored template or config defaults."""
from
backend.models
import
UserPermissions
template
=
get_default_permissions_template
(
db
)
perms
=
UserPermissions
(
user_id
=
user_id
)
for
field
in
PERMISSION_FIELDS
:
if
hasattr
(
perms
,
field
):
setattr
(
perms
,
field
,
template
.
get
(
field
,
DEFAULT_PERMISSIONS
.
get
(
field
)))
db
.
add
(
perms
)
db
.
commit
()
db
.
refresh
(
perms
)
return
perms
def
_perms_to_dict
(
perms
)
->
dict
:
"""Convert a UserPermissions ORM object to a dict."""
return
{
"can_use_web_search"
:
perms
.
can_use_web_search
,
"can_use_ui_design"
:
perms
.
can_use_ui_design
,
"can_use_knowledge_base"
:
perms
.
can_use_knowledge_base
,
"can_use_gitlab"
:
perms
.
can_use_gitlab
,
"can_use_attachments"
:
perms
.
can_use_attachments
,
"can_export_pptx"
:
perms
.
can_export_pptx
,
"can_export_docx"
:
perms
.
can_export_docx
,
"allowed_models"
:
perms
.
allowed_models
or
"all"
,
"max_tokens_cap"
:
perms
.
max_tokens_cap
or
4096
,
"max_reasoning_budget"
:
perms
.
max_reasoning_budget
or
0
,
"max_chats"
:
perms
.
max_chats
or
0
,
"max_messages_per_day"
:
perms
.
max_messages_per_day
or
0
,
"max_knowledge_bases"
:
perms
.
max_knowledge_bases
or
0
,
"max_documents_per_kb"
:
perms
.
max_documents_per_kb
or
0
,
"max_attachment_size_mb"
:
perms
.
max_attachment_size_mb
or
10
,
"max_attachments_per_message"
:
perms
.
max_attachments_per_message
or
5
,
}
def
check_feature
(
user_id
:
str
,
feature
:
str
,
db
:
Session
):
"""Check if user has access to a feature. Raises 403 if denied."""
perms
=
get_user_permissions
(
user_id
,
db
)
key
=
f
"can_{feature}"
if
not
feature
.
startswith
(
"can_"
)
else
feature
if
not
perms
.
get
(
key
,
False
):
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
f
"Feature '{feature.replace('can_', '').replace('use_', '')}' is not enabled for your account. Contact your admin."
,
)
def
check_model_allowed
(
user_id
:
str
,
model_id
:
str
,
db
:
Session
)
->
str
:
"""Validate model access. Returns the model_id if allowed, or falls back to first allowed model."""
perms
=
get_user_permissions
(
user_id
,
db
)
allowed
=
perms
.
get
(
"allowed_models"
,
"all"
)
if
allowed
==
"all"
:
return
model_id
allowed_list
=
[
m
.
strip
()
for
m
in
allowed
.
split
(
","
)
if
m
.
strip
()]
if
model_id
in
allowed_list
:
return
model_id
if
allowed_list
:
return
allowed_list
[
0
]
return
model_id
def
count_user_messages_today
(
user_id
:
str
,
db
:
Session
)
->
int
:
"""Count user messages sent today (UTC)."""
from
backend.models
import
Chat
,
Message
today_start
=
datetime
.
utcnow
()
.
replace
(
hour
=
0
,
minute
=
0
,
second
=
0
,
microsecond
=
0
)
return
(
db
.
query
(
Message
)
.
join
(
Chat
)
.
filter
(
Chat
.
user_id
==
user_id
,
Message
.
role
==
"user"
,
Message
.
created_at
>=
today_start
,
)
.
count
()
)
def
count_user_chats
(
user_id
:
str
,
db
:
Session
)
->
int
:
"""Count total chats for a user."""
from
backend.models
import
Chat
return
db
.
query
(
Chat
)
.
filter
(
Chat
.
user_id
==
user_id
)
.
count
()
def
count_user_knowledge_bases
(
user_id
:
str
,
db
:
Session
)
->
int
:
"""Count knowledge bases owned by user."""
from
backend.models
import
KnowledgeBase
return
db
.
query
(
KnowledgeBase
)
.
filter
(
KnowledgeBase
.
user_id
==
user_id
)
.
count
()
def
count_kb_documents
(
kb_id
:
str
,
db
:
Session
)
->
int
:
"""Count documents in a knowledge base."""
from
backend.models
import
KnowledgeDocument
return
db
.
query
(
KnowledgeDocument
)
.
filter
(
KnowledgeDocument
.
knowledge_base_id
==
kb_id
)
.
count
()
\ No newline at end of file
backend/config.py
View file @
738f4bcd
"""
"""
Application configuration — reads from environment variables.
Son of Anton v4.2.0 — Configuration
Son of Anton v4.0.0
"""
"""
import
os
import
os
import
secrets
BEDROCK_API_KEY
:
str
=
os
.
getenv
(
APP_VERSION
=
"4.2.0"
"BEDROCK_API_KEY"
,
os
.
getenv
(
"AWS_BEARER_TOKEN_BEDROCK"
,
""
),
)
AWS_REGION
:
str
=
os
.
getenv
(
"AWS_REGION"
,
"eu-central-1"
)
PRIMARY_MODEL
:
str
=
os
.
getenv
(
"PRIMARY_MODEL"
,
"eu.anthropic.claude-opus-4-6-v1"
)
FAST_MODEL
:
str
=
os
.
getenv
(
"FAST_MODEL"
,
"eu.anthropic.claude-haiku-4-5-20251001-v1:0"
)
JWT_SECRET
:
str
=
os
.
getenv
(
"JWT_SECRET"
,
secrets
.
token_hex
(
32
))
# ═══════════════════════════════════════════════════
JWT_ALGORITHM
:
str
=
"HS256"
# AWS Bedrock
JWT_EXPIRY_HOURS
:
int
=
72
# ═══════════════════════════════════════════════════
BEDROCK_API_KEY
=
os
.
getenv
(
"BEDROCK_API_KEY"
,
""
)
AWS_REGION
=
os
.
getenv
(
"AWS_REGION"
,
"eu-central-1"
)
BEDROCK_ENDPOINT
=
f
"https://bedrock-runtime.{AWS_REGION}.amazonaws.com"
SUPERADMIN_PASSWORD
:
str
=
os
.
getenv
(
"SUPERADMIN_PASSWORD"
,
"admin123"
)
# Models
PRIMARY_MODEL
=
os
.
getenv
(
"PRIMARY_MODEL"
,
"eu.anthropic.claude-opus-4-6-v1"
)
FAST_MODEL
=
os
.
getenv
(
"FAST_MODEL"
,
"eu.anthropic.claude-haiku-4-5-20251001-v1:0"
)
DATABASE_URL
:
str
=
os
.
getenv
(
"DATABASE_URL"
,
"sqlite:////data/sonofanton.db"
)
AVAILABLE_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"
},
]
CHROMADB_PATH
:
str
=
os
.
getenv
(
"CHROMADB_PATH"
,
"/data/chromadb"
)
# ═══════════════════════════════════════════════════
UPLOAD_PATH
:
str
=
os
.
getenv
(
"UPLOAD_PATH"
,
"/data/uploads"
)
# Auth
ATTACHMENT_PATH
:
str
=
os
.
getenv
(
"ATTACHMENT_PATH"
,
"/data/uploads/chat_attachments"
)
# ═══════════════════════════════════════════════════
JWT_SECRET
=
os
.
getenv
(
"JWT_SECRET"
,
"CreatedSystemsOverloadedFunctionsBySonOfAnton"
)
SUPERADMIN_PASSWORD
=
os
.
getenv
(
"SUPERADMIN_PASSWORD"
,
"admin123"
)
DEFAULT_QUOTA
:
int
=
int
(
os
.
getenv
(
"DEFAULT_QUOTA"
,
"2000000"
))
# ═══════════════════════════════════════════════════
MAX_UPLOAD_BYTES
:
int
=
int
(
os
.
getenv
(
"MAX_UPLOAD_MB"
,
"50"
))
*
1024
*
1024
# Database
MAX_ATTACHMENT_BYTES
:
int
=
int
(
os
.
getenv
(
"MAX_ATTACHMENT_MB"
,
"25"
))
*
1024
*
1024
# ═══════════════════════════════════════════════════
DATABASE_URL
=
os
.
getenv
(
"DATABASE_URL"
,
"sqlite:////data/sonofanton.db"
)
MAX_IMAGE_DIMENSION
:
int
=
1568
# ═══════════════════════════════════════════════════
MAX_VIDEO_FRAMES
:
int
=
6
# Quotas & Uploads
# ═══════════════════════════════════════════════════
DEFAULT_QUOTA
=
int
(
os
.
getenv
(
"DEFAULT_QUOTA"
,
"2000000"
))
MAX_UPLOAD_MB
=
int
(
os
.
getenv
(
"MAX_UPLOAD_MB"
,
"50"
))
MAX_UPLOAD_BYTES
=
MAX_UPLOAD_MB
*
1024
*
1024
MAX_ATTACHMENT_BYTES
=
MAX_UPLOAD_BYTES
BEDROCK_ENDPOINT
:
str
=
(
# ═══════════════════════════════════════════════════
f
"https://bedrock-runtime.{AWS_REGION}.amazonaws.com"
# Attachments
)
# ═══════════════════════════════════════════════════
ATTACHMENT_PATH
=
os
.
getenv
(
"ATTACHMENT_PATH"
,
"/data/uploads/chat_attachments"
)
MAX_IMAGE_DIMENSION
=
2048
MAX_VIDEO_FRAMES
=
6
# SerpAPI for web search
# ═══════════════════════════════════════════════════
SERPAPI_KEY
:
str
=
os
.
getenv
(
# ChromaDB / RAG
"SERPAPI_KEY"
,
# ═══════════════════════════════════════════════════
"0f9efa98fb0fe7b27af609e8dd80e04c4af1e098ec81fe628a6d63aaaebe8bd6"
,
CHROMADB_PATH
=
os
.
getenv
(
"CHROMADB_PATH"
,
"/data/chromadb"
)
)
APP_VERSION
:
str
=
"4.2.0"
# ═══════════════════════════════════════════════════
\ No newline at end of file
# Web Search (SerpAPI)
# ═══════════════════════════════════════════════════
SERPAPI_KEY
=
os
.
getenv
(
"SERPAPI_KEY"
,
""
)
# ═══════════════════════════════════════════════════
# PERMISSION DEFAULTS — applied to new regular users
# ═══════════════════════════════════════════════════
DEFAULT_PERMISSIONS
=
{
"can_use_web_search"
:
False
,
"can_use_ui_design"
:
False
,
"can_use_knowledge_base"
:
True
,
"can_use_gitlab"
:
False
,
"can_use_attachments"
:
True
,
"can_export_pptx"
:
True
,
"can_export_docx"
:
True
,
"allowed_models"
:
"eu.anthropic.claude-haiku-4-5-20251001-v1:0"
,
"max_tokens_cap"
:
4096
,
"max_reasoning_budget"
:
0
,
"max_chats"
:
50
,
"max_messages_per_day"
:
100
,
"max_knowledge_bases"
:
3
,
"max_documents_per_kb"
:
20
,
"max_attachment_size_mb"
:
10
,
"max_attachments_per_message"
:
5
,
}
SUPERADMIN_PERMISSIONS
=
{
"can_use_web_search"
:
True
,
"can_use_ui_design"
:
True
,
"can_use_knowledge_base"
:
True
,
"can_use_gitlab"
:
True
,
"can_use_attachments"
:
True
,
"can_export_pptx"
:
True
,
"can_export_docx"
:
True
,
"allowed_models"
:
"all"
,
"max_tokens_cap"
:
65536
,
"max_reasoning_budget"
:
32000
,
"max_chats"
:
0
,
"max_messages_per_day"
:
0
,
"max_knowledge_bases"
:
0
,
"max_documents_per_kb"
:
0
,
"max_attachment_size_mb"
:
50
,
"max_attachments_per_message"
:
20
,
}
PERMISSION_FIELDS
=
list
(
DEFAULT_PERMISSIONS
.
keys
())
\ No newline at end of file
backend/main.py
View file @
738f4bcd
"""
"""
Son of Anton v4.
1
.0 — Main FastAPI Application
Son of Anton v4.
2
.0 — Main FastAPI Application
"""
"""
import
time
import
time
...
@@ -48,6 +48,12 @@ def _run_migrations():
...
@@ -48,6 +48,12 @@ def _run_migrations():
from
backend.models
import
ChatAttachment
from
backend.models
import
ChatAttachment
ChatAttachment
.
__table__
.
create
(
bind
=
engine
,
checkfirst
=
True
)
ChatAttachment
.
__table__
.
create
(
bind
=
engine
,
checkfirst
=
True
)
# Create user_permissions table if missing
if
"user_permissions"
not
in
existing_tables
:
from
backend.models
import
UserPermissions
UserPermissions
.
__table__
.
create
(
bind
=
engine
,
checkfirst
=
True
)
print
(
" Created user_permissions table"
)
for
table_name
in
[
"gitlab_settings"
,
"linked_repos"
,
"pending_actions"
]:
for
table_name
in
[
"gitlab_settings"
,
"linked_repos"
,
"pending_actions"
]:
if
table_name
not
in
existing_tables
:
if
table_name
not
in
existing_tables
:
print
(
f
" Creating {table_name} table"
)
print
(
f
" Creating {table_name} table"
)
...
...
backend/models.py
View file @
738f4bcd
"""
"""
SQLAlchemy ORM models — Son of Anton v4.
0
.0
SQLAlchemy ORM models — Son of Anton v4.
2
.0
"""
"""
from
datetime
import
datetime
from
datetime
import
datetime
...
@@ -39,6 +39,46 @@ class User(Base):
...
@@ -39,6 +39,46 @@ class User(Base):
created_at
=
Column
(
DateTime
,
default
=
datetime
.
utcnow
)
created_at
=
Column
(
DateTime
,
default
=
datetime
.
utcnow
)
chats
=
relationship
(
"Chat"
,
back_populates
=
"user"
,
cascade
=
"all,delete-orphan"
)
chats
=
relationship
(
"Chat"
,
back_populates
=
"user"
,
cascade
=
"all,delete-orphan"
)
permissions
=
relationship
(
"UserPermissions"
,
back_populates
=
"user"
,
uselist
=
False
,
cascade
=
"all,delete-orphan"
,
)
class
UserPermissions
(
Base
):
__tablename__
=
"user_permissions"
id
=
Column
(
String
(
36
),
primary_key
=
True
,
default
=
new_id
)
user_id
=
Column
(
String
(
36
),
ForeignKey
(
"users.id"
,
ondelete
=
"CASCADE"
),
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
)
can_use_gitlab
=
Column
(
Boolean
,
default
=
False
)
can_use_attachments
=
Column
(
Boolean
,
default
=
True
)
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
)
max_messages_per_day
=
Column
(
Integer
,
default
=
100
)
max_knowledge_bases
=
Column
(
Integer
,
default
=
3
)
max_documents_per_kb
=
Column
(
Integer
,
default
=
20
)
max_attachment_size_mb
=
Column
(
Integer
,
default
=
10
)
max_attachments_per_message
=
Column
(
Integer
,
default
=
5
)
updated_at
=
Column
(
DateTime
,
default
=
datetime
.
utcnow
,
onupdate
=
datetime
.
utcnow
)
user
=
relationship
(
"User"
,
back_populates
=
"permissions"
)
class
Chat
(
Base
):
class
Chat
(
Base
):
...
@@ -128,7 +168,7 @@ class KnowledgeDocument(Base):
...
@@ -128,7 +168,7 @@ class KnowledgeDocument(Base):
# ═══════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════
# GitLab Integration Models
— v4.0.0
# GitLab Integration Models
# ═══════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════
class
GitLabSettings
(
Base
):
class
GitLabSettings
(
Base
):
...
...
backend/routes/admin_routes.py
View file @
738f4bcd
"""
"""
Superadmin routes: user management, stats,
oversight.
Superadmin routes: user management, stats,
permissions — v4.2.0
"""
"""
from
pydantic
import
BaseModel
from
pydantic
import
BaseModel
...
@@ -10,8 +10,12 @@ from sqlalchemy.orm import Session
...
@@ -10,8 +10,12 @@ from sqlalchemy.orm import Session
from
sqlalchemy
import
func
from
sqlalchemy
import
func
from
backend.database
import
get_db
from
backend.database
import
get_db
from
backend.models
import
User
,
Chat
,
Message
,
KnowledgeBase
from
backend.models
import
User
,
Chat
,
Message
,
KnowledgeBase
,
UserPermissions
from
backend.auth
import
require_superadmin
,
hash_password
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
()
router
=
APIRouter
()
...
@@ -32,6 +36,29 @@ class CreateUserBody(BaseModel):
...
@@ -32,6 +36,29 @@ class CreateUserBody(BaseModel):
quota_tokens_monthly
:
int
=
2_000_000
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"
)
@
router
.
get
(
"/stats"
)
def
get_stats
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
def
get_stats
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
return
{
return
{
...
@@ -79,6 +106,9 @@ def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin),
...
@@ -79,6 +106,9 @@ def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin),
)
)
db
.
add
(
user
)
db
.
add
(
user
)
db
.
commit
()
db
.
commit
()
db
.
refresh
(
user
)
# Auto-create permissions from defaults template
ensure_user_permissions
(
user
.
id
,
db
)
return
{
"id"
:
user
.
id
,
"username"
:
user
.
username
}
return
{
"id"
:
user
.
id
,
"username"
:
user
.
username
}
...
@@ -127,4 +157,78 @@ def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depe
...
@@ -127,4 +157,78 @@ def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depe
"message_count"
:
msg_count
,
"message_count"
:
msg_count
,
"updated_at"
:
str
(
c
.
updated_at
),
"updated_at"
:
str
(
c
.
updated_at
),
})
})
return
result
return
result
\ No newline at end of file
# ═══════════════════════════════════════════════════
# 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
()
return
get_user_permissions
(
user_id
,
db
)
def
_apply_permissions_body
(
perms
:
UserPermissions
,
body
:
PermissionsBody
):
"""Apply non-None fields from the body to a permissions object."""
data
=
body
.
dict
(
exclude_none
=
True
)
for
field
,
value
in
data
.
items
():
if
hasattr
(
perms
,
field
):
setattr
(
perms
,
field
,
value
)
\ No newline at end of file
backend/routes/attachment_routes.py
View file @
738f4bcd
"""
"""
Chat attachment upload, serve,
and delete routes
.
Chat attachment upload, serve,
delete — v4.2.0 with permission enforcement
.
"""
"""
import
os
import
os
...
@@ -11,7 +11,7 @@ from sqlalchemy.orm import Session
...
@@ -11,7 +11,7 @@ from sqlalchemy.orm import Session
from
backend.database
import
get_db
from
backend.database
import
get_db
from
backend.models
import
User
,
Chat
,
ChatAttachment
from
backend.models
import
User
,
Chat
,
ChatAttachment
from
backend.auth
import
get_current_user
,
decode_token
from
backend.auth
import
get_current_user
,
decode_token
,
get_user_permissions
,
check_feature
from
backend.services
import
attachment_service
from
backend.services
import
attachment_service
from
backend.config
import
MAX_ATTACHMENT_BYTES
from
backend.config
import
MAX_ATTACHMENT_BYTES
...
@@ -41,6 +41,15 @@ async def upload_attachments(
...
@@ -41,6 +41,15 @@ async def upload_attachments(
user
:
User
=
Depends
(
get_current_user
),
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
db
:
Session
=
Depends
(
get_db
),
):
):
check_feature
(
user
.
id
,
"use_attachments"
,
db
)
perms
=
get_user_permissions
(
user
.
id
,
db
)
max_per_msg
=
perms
.
get
(
"max_attachments_per_message"
,
5
)
if
len
(
files
)
>
max_per_msg
:
raise
HTTPException
(
400
,
f
"Too many files. Max {max_per_msg} per message."
)
max_size
=
perms
.
get
(
"max_attachment_size_mb"
,
10
)
*
1024
*
1024
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
if
not
chat
:
raise
HTTPException
(
404
,
"Chat not found"
)
raise
HTTPException
(
404
,
"Chat not found"
)
...
@@ -50,28 +59,22 @@ async def upload_attachments(
...
@@ -50,28 +59,22 @@ async def upload_attachments(
filename
=
file
.
filename
or
"file"
filename
=
file
.
filename
or
"file"
try
:
try
:
content
=
await
file
.
read
()
content
=
await
file
.
read
()
if
len
(
content
)
>
MAX_ATTACHMENT_BYTES
:
if
len
(
content
)
>
max_size
:
results
.
append
({
"error"
:
f
"Too large: {filename}"
})
results
.
append
({
"error"
:
f
"Too large: {filename}
({len(content) // 1024 // 1024}MB). Your limit: {perms.get('max_attachment_size_mb', 10)}MB.
"
})
continue
continue
meta
=
attachment_service
.
save_attachment
(
meta
=
attachment_service
.
save_attachment
(
chat_id
=
chat_id
,
filename
=
filename
,
chat_id
=
chat_id
,
filename
=
filename
,
content
=
content
,
content_type
=
file
.
content_type
,
content
=
content
,
content_type
=
file
.
content_type
,
)
)
att
=
ChatAttachment
(
att
=
ChatAttachment
(
id
=
meta
[
"id"
],
chat_id
=
chat_id
,
id
=
meta
[
"id"
],
chat_id
=
chat_id
,
filename
=
meta
[
"filename"
],
filename
=
meta
[
"filename"
],
original_filename
=
meta
[
"original_filename"
],
original_filename
=
meta
[
"original_filename"
],
mime_type
=
meta
[
"mime_type"
],
file_type
=
meta
[
"file_type"
],
mime_type
=
meta
[
"mime_type"
],
file_size
=
meta
[
"file_size"
],
storage_path
=
meta
[
"storage_path"
],
file_type
=
meta
[
"file_type"
],
file_size
=
meta
[
"file_size"
],
storage_path
=
meta
[
"storage_path"
],
text_extract
=
meta
.
get
(
"text_extract"
),
text_extract
=
meta
.
get
(
"text_extract"
),
)
)
db
.
add
(
att
)
db
.
add
(
att
);
db
.
commit
();
db
.
refresh
(
att
)
db
.
commit
()
db
.
refresh
(
att
)
results
.
append
(
_att_dict
(
att
))
results
.
append
(
_att_dict
(
att
))
except
Exception
as
e
:
except
Exception
as
e
:
results
.
append
({
"error"
:
f
"Failed: {filename}: {str(e)}"
})
results
.
append
({
"error"
:
f
"Failed: {filename}: {str(e)}"
})
...
@@ -80,16 +83,10 @@ async def upload_attachments(
...
@@ -80,16 +83,10 @@ async def upload_attachments(
@
router
.
get
(
"/attachments/{attachment_id}/file"
)
@
router
.
get
(
"/attachments/{attachment_id}/file"
)
def
serve_attachment
(
def
serve_attachment
(
attachment_id
:
str
,
request
:
Request
,
token
:
Optional
[
str
]
=
Query
(
None
),
db
:
Session
=
Depends
(
get_db
)):
attachment_id
:
str
,
request
:
Request
,
token
:
Optional
[
str
]
=
Query
(
None
),
db
:
Session
=
Depends
(
get_db
),
):
user
=
_get_user_flexible
(
request
,
db
,
token
)
user
=
_get_user_flexible
(
request
,
db
,
token
)
att
=
db
.
query
(
ChatAttachment
)
.
filter
(
ChatAttachment
.
id
==
attachment_id
)
.
first
()
att
=
db
.
query
(
ChatAttachment
)
.
filter
(
ChatAttachment
.
id
==
attachment_id
)
.
first
()
if
not
att
:
if
not
att
:
raise
HTTPException
(
404
,
"Attachment not found"
)
raise
HTTPException
(
404
,
"Attachment not found"
)
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
att
.
chat_id
)
.
first
()
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
att
.
chat_id
)
.
first
()
if
not
chat
or
(
chat
.
user_id
!=
user
.
id
and
user
.
role
!=
"superadmin"
):
if
not
chat
or
(
chat
.
user_id
!=
user
.
id
and
user
.
role
!=
"superadmin"
):
raise
HTTPException
(
403
,
"Access denied"
)
raise
HTTPException
(
403
,
"Access denied"
)
...
@@ -99,20 +96,14 @@ def serve_attachment(
...
@@ -99,20 +96,14 @@ def serve_attachment(
@
router
.
delete
(
"/attachments/{attachment_id}"
)
@
router
.
delete
(
"/attachments/{attachment_id}"
)
def
delete_attachment
(
def
delete_attachment
(
attachment_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
attachment_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
att
=
db
.
query
(
ChatAttachment
)
.
filter
(
ChatAttachment
.
id
==
attachment_id
)
.
first
()
att
=
db
.
query
(
ChatAttachment
)
.
filter
(
ChatAttachment
.
id
==
attachment_id
)
.
first
()
if
not
att
:
if
not
att
:
raise
HTTPException
(
404
)
raise
HTTPException
(
404
)
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
att
.
chat_id
)
.
first
()
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
att
.
chat_id
)
.
first
()
if
not
chat
or
(
chat
.
user_id
!=
user
.
id
and
user
.
role
!=
"superadmin"
):
if
not
chat
or
(
chat
.
user_id
!=
user
.
id
and
user
.
role
!=
"superadmin"
):
raise
HTTPException
(
403
)
raise
HTTPException
(
403
)
attachment_service
.
delete_attachment_file
(
att
.
storage_path
)
attachment_service
.
delete_attachment_file
(
att
.
storage_path
)
db
.
delete
(
att
)
db
.
delete
(
att
);
db
.
commit
()
db
.
commit
()
return
{
"ok"
:
True
}
return
{
"ok"
:
True
}
...
@@ -122,4 +113,4 @@ def _att_dict(att):
...
@@ -122,4 +113,4 @@ def _att_dict(att):
"filename"
:
att
.
filename
,
"original_filename"
:
att
.
original_filename
,
"filename"
:
att
.
filename
,
"original_filename"
:
att
.
original_filename
,
"mime_type"
:
att
.
mime_type
,
"file_type"
:
att
.
file_type
,
"mime_type"
:
att
.
mime_type
,
"file_type"
:
att
.
file_type
,
"file_size"
:
att
.
file_size
,
"created_at"
:
str
(
att
.
created_at
),
"file_size"
:
att
.
file_size
,
"created_at"
:
str
(
att
.
created_at
),
}
}
\ No newline at end of file
backend/routes/auth_routes.py
View file @
738f4bcd
"""
"""
Authentication routes: register, login, profile.
Authentication routes: register, login, profile
— with permissions
.
"""
"""
from
pydantic
import
BaseModel
,
EmailStr
from
pydantic
import
BaseModel
from
fastapi
import
APIRouter
,
Depends
,
HTTPException
,
status
from
fastapi
import
APIRouter
,
Depends
,
HTTPException
,
status
from
sqlalchemy.orm
import
Session
from
sqlalchemy.orm
import
Session
...
@@ -10,6 +10,7 @@ from backend.database import get_db
...
@@ -10,6 +10,7 @@ from backend.database import get_db
from
backend.models
import
User
from
backend.models
import
User
from
backend.auth
import
(
from
backend.auth
import
(
hash_password
,
verify_password
,
create_token
,
get_current_user
,
hash_password
,
verify_password
,
create_token
,
get_current_user
,
get_user_permissions
,
ensure_user_permissions
,
)
)
from
backend
import
config
from
backend
import
config
...
@@ -27,20 +28,6 @@ class LoginBody(BaseModel):
...
@@ -27,20 +28,6 @@ class LoginBody(BaseModel):
password
:
str
password
:
str
class
ProfileOut
(
BaseModel
):
id
:
str
username
:
str
email
:
str
role
:
str
is_active
:
bool
quota_tokens_monthly
:
int
tokens_used_this_month
:
int
created_at
:
str
class
Config
:
from_attributes
=
True
@
router
.
post
(
"/register"
)
@
router
.
post
(
"/register"
)
def
register
(
body
:
RegisterBody
,
db
:
Session
=
Depends
(
get_db
)):
def
register
(
body
:
RegisterBody
,
db
:
Session
=
Depends
(
get_db
)):
if
db
.
query
(
User
)
.
filter
(
if
db
.
query
(
User
)
.
filter
(
...
@@ -58,8 +45,13 @@ def register(body: RegisterBody, db: Session = Depends(get_db)):
...
@@ -58,8 +45,13 @@ def register(body: RegisterBody, db: Session = Depends(get_db)):
db
.
add
(
user
)
db
.
add
(
user
)
db
.
commit
()
db
.
commit
()
db
.
refresh
(
user
)
db
.
refresh
(
user
)
# Auto-create permissions from defaults template
ensure_user_permissions
(
user
.
id
,
db
)
token
=
create_token
(
user
.
id
,
user
.
role
)
token
=
create_token
(
user
.
id
,
user
.
role
)
return
{
"token"
:
token
,
"user"
:
_user_dict
(
user
)}
perms
=
get_user_permissions
(
user
.
id
,
db
)
return
{
"token"
:
token
,
"user"
:
_user_dict
(
user
,
perms
)}
@
router
.
post
(
"/login"
)
@
router
.
post
(
"/login"
)
...
@@ -70,16 +62,18 @@ def login(body: LoginBody, db: Session = Depends(get_db)):
...
@@ -70,16 +62,18 @@ def login(body: LoginBody, db: Session = Depends(get_db)):
if
not
user
.
is_active
:
if
not
user
.
is_active
:
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Account disabled"
)
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Account disabled"
)
token
=
create_token
(
user
.
id
,
user
.
role
)
token
=
create_token
(
user
.
id
,
user
.
role
)
return
{
"token"
:
token
,
"user"
:
_user_dict
(
user
)}
perms
=
get_user_permissions
(
user
.
id
,
db
)
return
{
"token"
:
token
,
"user"
:
_user_dict
(
user
,
perms
)}
@
router
.
get
(
"/me"
)
@
router
.
get
(
"/me"
)
def
me
(
user
:
User
=
Depends
(
get_current_user
)):
def
me
(
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
return
_user_dict
(
user
)
perms
=
get_user_permissions
(
user
.
id
,
db
)
return
_user_dict
(
user
,
perms
)
def
_user_dict
(
u
:
User
)
->
dict
:
def
_user_dict
(
u
:
User
,
perms
:
dict
=
None
)
->
dict
:
return
{
d
=
{
"id"
:
u
.
id
,
"id"
:
u
.
id
,
"username"
:
u
.
username
,
"username"
:
u
.
username
,
"email"
:
u
.
email
,
"email"
:
u
.
email
,
...
@@ -88,4 +82,7 @@ def _user_dict(u: User) -> dict:
...
@@ -88,4 +82,7 @@ def _user_dict(u: User) -> dict:
"quota_tokens_monthly"
:
u
.
quota_tokens_monthly
,
"quota_tokens_monthly"
:
u
.
quota_tokens_monthly
,
"tokens_used_this_month"
:
u
.
tokens_used_this_month
,
"tokens_used_this_month"
:
u
.
tokens_used_this_month
,
"created_at"
:
str
(
u
.
created_at
),
"created_at"
:
str
(
u
.
created_at
),
}
}
\ No newline at end of file
if
perms
is
not
None
:
d
[
"permissions"
]
=
perms
return
d
\ No newline at end of file
backend/routes/chat_routes.py
View file @
738f4bcd
"""
"""
Chat CRUD + message streaming — v4.
1.0 with web search suppor
t.
Chat CRUD + message streaming — v4.
2.0 with permission enforcemen
t.
"""
"""
import
json
import
json
...
@@ -13,7 +13,10 @@ from sqlalchemy.orm import Session
...
@@ -13,7 +13,10 @@ from sqlalchemy.orm import Session
from
backend.database
import
get_db
from
backend.database
import
get_db
from
backend.models
import
User
,
Chat
,
Message
,
ChatAttachment
,
LinkedRepo
,
GitLabSettings
from
backend.models
import
User
,
Chat
,
Message
,
ChatAttachment
,
LinkedRepo
,
GitLabSettings
from
backend.auth
import
get_current_user
from
backend.auth
import
(
get_current_user
,
get_user_permissions
,
check_feature
,
check_model_allowed
,
count_user_chats
,
count_user_messages_today
,
)
from
backend.services
import
attachment_service
,
gitlab_service
from
backend.services
import
attachment_service
,
gitlab_service
from
backend.services.generation_manager
import
manager
as
gen_manager
from
backend.services.generation_manager
import
manager
as
gen_manager
...
@@ -62,7 +65,32 @@ def list_chats(user: User = Depends(get_current_user), db: Session = Depends(get
...
@@ -62,7 +65,32 @@ def list_chats(user: User = Depends(get_current_user), db: Session = Depends(get
@
router
.
post
(
""
)
@
router
.
post
(
""
)
def
create_chat
(
body
:
CreateChatBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
def
create_chat
(
body
:
CreateChatBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
chat
=
Chat
(
user_id
=
user
.
id
,
title
=
body
.
title
,
model
=
body
.
model
,
knowledge_base_id
=
body
.
knowledge_base_id
or
None
,
linked_repo_id
=
body
.
linked_repo_id
or
None
,
max_tokens
=
body
.
max_tokens
,
reasoning_budget
=
body
.
reasoning_budget
)
perms
=
get_user_permissions
(
user
.
id
,
db
)
# Enforce max chats
max_chats
=
perms
.
get
(
"max_chats"
,
0
)
if
max_chats
>
0
:
current_count
=
count_user_chats
(
user
.
id
,
db
)
if
current_count
>=
max_chats
:
raise
HTTPException
(
403
,
f
"Chat limit reached ({max_chats}). Delete old chats or contact admin."
)
# Validate KB permission
if
body
.
knowledge_base_id
:
if
not
perms
.
get
(
"can_use_knowledge_base"
):
raise
HTTPException
(
403
,
"Knowledge base access not enabled for your account."
)
# Validate GitLab permission
if
body
.
linked_repo_id
:
if
not
perms
.
get
(
"can_use_gitlab"
):
raise
HTTPException
(
403
,
"GitLab access not enabled for your account."
)
chat
=
Chat
(
user_id
=
user
.
id
,
title
=
body
.
title
,
model
=
check_model_allowed
(
user
.
id
,
body
.
model
,
db
),
knowledge_base_id
=
body
.
knowledge_base_id
or
None
,
linked_repo_id
=
body
.
linked_repo_id
or
None
,
max_tokens
=
min
(
body
.
max_tokens
,
perms
.
get
(
"max_tokens_cap"
,
4096
)),
reasoning_budget
=
min
(
body
.
reasoning_budget
,
perms
.
get
(
"max_reasoning_budget"
,
0
)),
)
db
.
add
(
chat
);
db
.
commit
();
db
.
refresh
(
chat
)
db
.
add
(
chat
);
db
.
commit
();
db
.
refresh
(
chat
)
return
_chat_dict
(
chat
,
db
)
return
_chat_dict
(
chat
,
db
)
...
@@ -78,12 +106,19 @@ def get_chat(chat_id: str, user: User = Depends(get_current_user), db: Session =
...
@@ -78,12 +106,19 @@ def get_chat(chat_id: str, user: User = Depends(get_current_user), db: Session =
def
update_chat
(
chat_id
:
str
,
body
:
UpdateChatBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
def
update_chat
(
chat_id
:
str
,
body
:
UpdateChatBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
raise
HTTPException
(
404
)
if
not
chat
:
raise
HTTPException
(
404
)
perms
=
get_user_permissions
(
user
.
id
,
db
)
if
body
.
title
is
not
None
:
chat
.
title
=
body
.
title
if
body
.
title
is
not
None
:
chat
.
title
=
body
.
title
if
body
.
model
is
not
None
:
chat
.
model
=
body
.
model
if
body
.
model
is
not
None
:
chat
.
model
=
check_model_allowed
(
user
.
id
,
body
.
model
,
db
)
if
body
.
max_tokens
is
not
None
:
chat
.
max_tokens
=
body
.
max_tokens
if
body
.
max_tokens
is
not
None
:
chat
.
max_tokens
=
min
(
body
.
max_tokens
,
perms
.
get
(
"max_tokens_cap"
,
4096
))
if
body
.
reasoning_budget
is
not
None
:
chat
.
reasoning_budget
=
body
.
reasoning_budget
if
body
.
reasoning_budget
is
not
None
:
chat
.
reasoning_budget
=
min
(
body
.
reasoning_budget
,
perms
.
get
(
"max_reasoning_budget"
,
0
))
if
body
.
knowledge_base_id
is
not
None
:
chat
.
knowledge_base_id
=
body
.
knowledge_base_id
or
None
if
body
.
knowledge_base_id
is
not
None
:
if
body
.
linked_repo_id
is
not
None
:
chat
.
linked_repo_id
=
body
.
linked_repo_id
or
None
if
body
.
knowledge_base_id
and
not
perms
.
get
(
"can_use_knowledge_base"
):
raise
HTTPException
(
403
,
"Knowledge base not enabled."
)
chat
.
knowledge_base_id
=
body
.
knowledge_base_id
or
None
if
body
.
linked_repo_id
is
not
None
:
if
body
.
linked_repo_id
and
not
perms
.
get
(
"can_use_gitlab"
):
raise
HTTPException
(
403
,
"GitLab not enabled."
)
chat
.
linked_repo_id
=
body
.
linked_repo_id
or
None
db
.
commit
()
db
.
commit
()
return
_chat_dict
(
chat
,
db
)
return
_chat_dict
(
chat
,
db
)
...
@@ -128,14 +163,41 @@ async def reconnect_stream(chat_id: str, user: User = Depends(get_current_user))
...
@@ -128,14 +163,41 @@ async def reconnect_stream(chat_id: str, user: User = Depends(get_current_user))
@
router
.
post
(
"/{chat_id}/messages"
)
@
router
.
post
(
"/{chat_id}/messages"
)
async
def
send_message
(
chat_id
:
str
,
body
:
SendMessageBody
,
user
:
User
=
Depends
(
get_current_user
)):
async
def
send_message
(
chat_id
:
str
,
body
:
SendMessageBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
perms
=
get_user_permissions
(
user
.
id
,
db
)
# Enforce daily message limit
max_per_day
=
perms
.
get
(
"max_messages_per_day"
,
0
)
if
max_per_day
>
0
:
today_count
=
count_user_messages_today
(
user
.
id
,
db
)
if
today_count
>=
max_per_day
:
raise
HTTPException
(
429
,
f
"Daily message limit reached ({max_per_day}). Try again tomorrow."
)
# Enforce web search permission
web_search
=
body
.
web_search
if
web_search
and
not
perms
.
get
(
"can_use_web_search"
):
web_search
=
False
# Enforce attachment permission
if
body
.
attachment_ids
and
not
perms
.
get
(
"can_use_attachments"
):
raise
HTTPException
(
403
,
"File attachments not enabled for your account."
)
if
body
.
attachment_ids
:
max_att
=
perms
.
get
(
"max_attachments_per_message"
,
5
)
if
len
(
body
.
attachment_ids
)
>
max_att
:
raise
HTTPException
(
400
,
f
"Too many attachments. Max {max_att} per message."
)
# Enforce model & limits
model
=
check_model_allowed
(
user
.
id
,
body
.
model
or
"eu.anthropic.claude-opus-4-6-v1"
,
db
)
max_tokens
=
min
(
body
.
max_tokens
,
perms
.
get
(
"max_tokens_cap"
,
4096
))
reasoning_budget
=
min
(
body
.
reasoning_budget
,
perms
.
get
(
"max_reasoning_budget"
,
0
))
gen_manager
.
start
(
gen_manager
.
start
(
chat_id
=
chat_id
,
user_id
=
user
.
id
,
content
=
body
.
content
,
chat_id
=
chat_id
,
user_id
=
user
.
id
,
content
=
body
.
content
,
model
=
body
.
model
or
"eu.anthropic.claude-opus-4-6-v1"
,
model
=
model
,
max_tokens
=
max_tokens
,
reasoning_budget
=
reasoning_budget
,
max_tokens
=
body
.
max_tokens
,
reasoning_budget
=
body
.
reasoning_budget
,
knowledge_base_id
=
body
.
knowledge_base_id
,
knowledge_base_id
=
body
.
knowledge_base_id
,
attachment_ids
=
body
.
attachment_ids
,
attachment_ids
=
body
.
attachment_ids
,
web_search
=
body
.
web_search
,
web_search
=
web_search
,
)
)
async
def
generate
():
async
def
generate
():
async
for
event
in
gen_manager
.
stream_events
(
chat_id
):
async
for
event
in
gen_manager
.
stream_events
(
chat_id
):
...
@@ -145,6 +207,7 @@ async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends
...
@@ -145,6 +207,7 @@ async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends
@
router
.
post
(
"/{chat_id}/commit"
)
@
router
.
post
(
"/{chat_id}/commit"
)
async
def
commit_from_chat
(
chat_id
:
str
,
body
:
CommitFromChatBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
async
def
commit_from_chat
(
chat_id
:
str
,
body
:
CommitFromChatBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
check_feature
(
user
.
id
,
"use_gitlab"
,
db
)
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
raise
HTTPException
(
404
,
"Chat not found"
)
if
not
chat
:
raise
HTTPException
(
404
,
"Chat not found"
)
if
not
chat
.
linked_repo_id
:
raise
HTTPException
(
400
,
"No repository linked"
)
if
not
chat
.
linked_repo_id
:
raise
HTTPException
(
400
,
"No repository linked"
)
...
...
backend/routes/export_routes.py
View file @
738f4bcd
"""
"""
Export routes — PPTX and DOCX
generation from markdown
.
Export routes — PPTX and DOCX
with permission checks
.
"""
"""
import
re
import
re
...
@@ -10,10 +10,12 @@ from fastapi import APIRouter, Depends
...
@@ -10,10 +10,12 @@ from fastapi import APIRouter, Depends
from
fastapi.responses
import
StreamingResponse
from
fastapi.responses
import
StreamingResponse
import
io
import
io
from
backend.auth
import
get_current_user
from
backend.database
import
get_db
from
backend.auth
import
get_current_user
,
check_feature
from
backend.models
import
User
from
backend.models
import
User
from
backend.services.pptx_service
import
generate_pptx
from
backend.services.pptx_service
import
generate_pptx
from
backend.services.docx_service
import
generate_docx
from
backend.services.docx_service
import
generate_docx
from
sqlalchemy.orm
import
Session
router
=
APIRouter
()
router
=
APIRouter
()
...
@@ -32,7 +34,8 @@ def _safe_filename(title: str, ext: str) -> str:
...
@@ -32,7 +34,8 @@ def _safe_filename(title: str, ext: str) -> str:
@
router
.
post
(
"/pptx"
)
@
router
.
post
(
"/pptx"
)
def
export_pptx
(
body
:
ExportBody
,
user
:
User
=
Depends
(
get_current_user
)):
def
export_pptx
(
body
:
ExportBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
check_feature
(
user
.
id
,
"export_pptx"
,
db
)
title
=
body
.
title
or
"Presentation"
title
=
body
.
title
or
"Presentation"
data
=
generate_pptx
(
body
.
markdown
,
title
)
data
=
generate_pptx
(
body
.
markdown
,
title
)
filename
=
_safe_filename
(
title
,
"pptx"
)
filename
=
_safe_filename
(
title
,
"pptx"
)
...
@@ -44,7 +47,8 @@ def export_pptx(body: ExportBody, user: User = Depends(get_current_user)):
...
@@ -44,7 +47,8 @@ def export_pptx(body: ExportBody, user: User = Depends(get_current_user)):
@
router
.
post
(
"/docx"
)
@
router
.
post
(
"/docx"
)
def
export_docx
(
body
:
ExportBody
,
user
:
User
=
Depends
(
get_current_user
)):
def
export_docx
(
body
:
ExportBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
check_feature
(
user
.
id
,
"export_docx"
,
db
)
title
=
body
.
title
or
"Document"
title
=
body
.
title
or
"Document"
data
=
generate_docx
(
body
.
markdown
,
title
)
data
=
generate_docx
(
body
.
markdown
,
title
)
filename
=
_safe_filename
(
title
,
"docx"
)
filename
=
_safe_filename
(
title
,
"docx"
)
...
...
backend/routes/knowledge_routes.py
View file @
738f4bcd
This diff is collapsed.
Click to expand it.
backend/seed.py
View file @
738f4bcd
"""
"""
Seed
the superadmin account on first startup
.
Seed
superadmin user and default permissions template
.
"""
"""
from
backend.database
import
SessionLocal
from
backend.database
import
SessionLocal
from
backend.models
import
User
from
backend.models
import
User
,
UserPermissions
from
backend.auth
import
hash_password
from
backend.auth
import
hash_password
from
backend
import
config
from
backend
.config
import
SUPERADMIN_PASSWORD
,
SUPERADMIN_PERMISSIONS
,
DEFAULT_PERMISSIONS
,
PERMISSION_FIELDS
def
seed_superadmin
():
def
seed_superadmin
():
...
@@ -13,17 +13,50 @@ def seed_superadmin():
...
@@ -13,17 +13,50 @@ def seed_superadmin():
try
:
try
:
existing
=
db
.
query
(
User
)
.
filter
(
User
.
username
==
"superadmin"
)
.
first
()
existing
=
db
.
query
(
User
)
.
filter
(
User
.
username
==
"superadmin"
)
.
first
()
if
not
existing
:
if
not
existing
:
admin
=
User
(
user
=
User
(
username
=
"superadmin"
,
username
=
"superadmin"
,
email
=
"admin@sonofanton.local"
,
email
=
"admin@sonofanton.local"
,
password_hash
=
hash_password
(
config
.
SUPERADMIN_PASSWORD
),
password_hash
=
hash_password
(
SUPERADMIN_PASSWORD
),
role
=
"superadmin"
,
role
=
"superadmin"
,
quota_tokens_monthly
=
999_999_999
,
quota_tokens_monthly
=
999_999_999
,
)
)
db
.
add
(
admin
)
db
.
add
(
user
)
db
.
commit
()
db
.
commit
()
print
(
"✅ Superadmin account created."
)
db
.
refresh
(
user
)
print
(
f
" Created superadmin (password: {SUPERADMIN_PASSWORD})"
)
# Create superadmin permissions
perms
=
UserPermissions
(
user_id
=
user
.
id
)
for
field
in
PERMISSION_FIELDS
:
if
hasattr
(
perms
,
field
):
setattr
(
perms
,
field
,
SUPERADMIN_PERMISSIONS
.
get
(
field
))
db
.
add
(
perms
)
db
.
commit
()
print
(
" Created superadmin permissions"
)
else
:
else
:
print
(
"ℹ️ Superadmin account already exists."
)
# Ensure superadmin has permissions row
sp
=
db
.
query
(
UserPermissions
)
.
filter
(
UserPermissions
.
user_id
==
existing
.
id
)
.
first
()
if
not
sp
:
perms
=
UserPermissions
(
user_id
=
existing
.
id
)
for
field
in
PERMISSION_FIELDS
:
if
hasattr
(
perms
,
field
):
setattr
(
perms
,
field
,
SUPERADMIN_PERMISSIONS
.
get
(
field
))
db
.
add
(
perms
)
db
.
commit
()
print
(
" Created superadmin permissions (existing user)"
)
# Create/update defaults template (special row with user_id = "__defaults__")
defaults
=
db
.
query
(
UserPermissions
)
.
filter
(
UserPermissions
.
user_id
==
"__defaults__"
)
.
first
()
if
not
defaults
:
defaults
=
UserPermissions
(
user_id
=
"__defaults__"
)
for
field
in
PERMISSION_FIELDS
:
if
hasattr
(
defaults
,
field
):
setattr
(
defaults
,
field
,
DEFAULT_PERMISSIONS
.
get
(
field
))
db
.
add
(
defaults
)
db
.
commit
()
print
(
" Created default permissions template"
)
except
Exception
as
e
:
print
(
f
" Seed error: {e}"
)
finally
:
finally
:
db
.
close
()
db
.
close
()
\ No newline at end of file
frontend/src/api.js
View file @
738f4bcd
...
@@ -65,59 +65,33 @@ export const adminUpdateUser = (t, id, d) => request("PUT", `/admin/users/${id}`
...
@@ -65,59 +65,33 @@ export const adminUpdateUser = (t, id, d) => request("PUT", `/admin/users/${id}`
export
const
adminDeleteUser
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/admin/users/
${
id
}
`
,
t
);
export
const
adminDeleteUser
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/admin/users/
${
id
}
`
,
t
);
export
const
adminListChats
=
(
t
)
=>
request
(
"GET"
,
"/admin/chats"
,
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
// 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
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 PPTX / DOCX
export
async
function
exportPptx
(
token
,
markdown
,
title
)
{
export
async
function
exportPptx
(
token
,
markdown
,
title
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/export/pptx`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
,
title
})
});
const
res
=
await
fetch
(
`
${
BASE
}
/export/pptx`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
,
title
})
});
if
(
!
res
.
ok
)
throw
new
Error
(
"PPTX export failed"
);
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"PPTX export failed"
));
}
const
blob
=
await
res
.
blob
();
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
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"
;
const
safe
=
(
title
||
"presentation"
).
replace
(
/
[^\w\s
-
]
/g
,
""
).
trim
().
replace
(
/
\s
+/g
,
"-"
).
slice
(
0
,
50
)
||
"presentation"
;
a
.
download
=
`
${
safe
}
.pptx`
;
a
.
download
=
`
${
safe
}
.pptx`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
}
export
async
function
exportDocx
(
token
,
markdown
,
title
)
{
export
async
function
exportDocx
(
token
,
markdown
,
title
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/export/docx`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
,
title
})
});
const
res
=
await
fetch
(
`
${
BASE
}
/export/docx`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
,
title
})
});
if
(
!
res
.
ok
)
throw
new
Error
(
"DOCX export failed"
);
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"DOCX export failed"
));
}
const
blob
=
await
res
.
blob
();
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
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"
;
const
safe
=
(
title
||
"document"
).
replace
(
/
[^\w\s
-
]
/g
,
""
).
trim
().
replace
(
/
\s
+/g
,
"-"
).
slice
(
0
,
50
)
||
"document"
;
a
.
download
=
`
${
safe
}
.docx`
;
a
.
download
=
`
${
safe
}
.docx`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
}
// Utilities
// Utilities
const
CODE_BLOCK_RE
=
/```
(\S
*
?)(?:
:
(\S
+
?))?\s
*
?\n([\s\S]
*
?)
```/g
;
const
CODE_BLOCK_RE
=
/
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
;
}
\ No newline at end of file
// 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
frontend/src/components/ChatView.jsx
View file @
738f4bcd
This diff is collapsed.
Click to expand it.
frontend/src/components/Sidebar.jsx
View file @
738f4bcd
import
React
,
{
useState
,
useEffect
}
from
"react"
;
import
React
,
{
useState
,
useEffect
}
from
"react"
;
import
{
useApp
}
from
"../store"
;
import
{
useApp
,
usePermissions
}
from
"../store"
;
import
{
useNavigate
}
from
"react-router-dom"
;
import
{
useNavigate
}
from
"react-router-dom"
;
import
{
listChats
,
createChat
,
deleteChat
,
renameChat
}
from
"../api"
;
import
{
listChats
,
createChat
,
deleteChat
,
renameChat
}
from
"../api"
;
import
{
import
{
...
@@ -9,6 +9,7 @@ import {
...
@@ -9,6 +9,7 @@ import {
export
default
function
Sidebar
({
mobile
,
onClose
})
{
export
default
function
Sidebar
({
mobile
,
onClose
})
{
const
{
state
,
dispatch
}
=
useApp
();
const
{
state
,
dispatch
}
=
useApp
();
const
perms
=
usePermissions
();
const
nav
=
useNavigate
();
const
nav
=
useNavigate
();
const
activeChatId
=
state
.
activeChatId
;
const
activeChatId
=
state
.
activeChatId
;
const
[
editId
,
setEditId
]
=
useState
(
null
);
const
[
editId
,
setEditId
]
=
useState
(
null
);
...
@@ -32,24 +33,18 @@ export default function Sidebar({ mobile, onClose }) {
...
@@ -32,24 +33,18 @@ export default function Sidebar({ mobile, onClose }) {
try
{
try
{
const
chat
=
await
createChat
(
state
.
token
);
const
chat
=
await
createChat
(
state
.
token
);
dispatch
({
type
:
"ADD_CHAT"
,
chat
});
dispatch
({
type
:
"ADD_CHAT"
,
chat
});
}
catch
{
}
}
catch
(
e
)
{
alert
(
e
.
message
);
}
}
}
async
function
handleDelete
(
e
,
chatId
)
{
async
function
handleDelete
(
e
,
chatId
)
{
e
.
stopPropagation
();
e
.
stopPropagation
();
if
(
!
confirm
(
"Delete this chat?"
))
return
;
if
(
!
confirm
(
"Delete this chat?"
))
return
;
try
{
try
{
await
deleteChat
(
state
.
token
,
chatId
);
dispatch
({
type
:
"REMOVE_CHAT"
,
chatId
});
}
catch
{
}
await
deleteChat
(
state
.
token
,
chatId
);
dispatch
({
type
:
"REMOVE_CHAT"
,
chatId
});
}
catch
{
}
}
}
async
function
handleRename
(
chatId
)
{
async
function
handleRename
(
chatId
)
{
if
(
!
editTitle
.
trim
())
{
setEditId
(
null
);
return
;
}
if
(
!
editTitle
.
trim
())
{
setEditId
(
null
);
return
;
}
try
{
try
{
await
renameChat
(
state
.
token
,
chatId
,
editTitle
.
trim
());
dispatch
({
type
:
"UPDATE_CHAT"
,
chat
:
{
id
:
chatId
,
title
:
editTitle
.
trim
()
}
});
}
catch
{
}
await
renameChat
(
state
.
token
,
chatId
,
editTitle
.
trim
());
dispatch
({
type
:
"UPDATE_CHAT"
,
chat
:
{
id
:
chatId
,
title
:
editTitle
.
trim
()
}
});
}
catch
{
}
setEditId
(
null
);
setEditId
(
null
);
}
}
...
@@ -57,7 +52,6 @@ export default function Sidebar({ mobile, onClose }) {
...
@@ -57,7 +52,6 @@ export default function Sidebar({ mobile, onClose }) {
return
(
return
(
<
div
className=
{
`${mobile ? "h-full" : "h-dvh"} w-72 bg-anton-surface border-r border-anton-border flex flex-col`
}
>
<
div
className=
{
`${mobile ? "h-full" : "h-dvh"} w-72 bg-anton-surface border-r border-anton-border flex flex-col`
}
>
{
/* Header */
}
<
div
className=
"p-3 border-b border-anton-border"
>
<
div
className=
"p-3 border-b border-anton-border"
>
<
div
className=
"flex items-center gap-2 mb-3"
>
<
div
className=
"flex items-center gap-2 mb-3"
>
<
div
className=
"w-8 h-8 rounded-lg bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center"
>
<
div
className=
"w-8 h-8 rounded-lg bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center"
>
...
@@ -65,7 +59,7 @@ export default function Sidebar({ mobile, onClose }) {
...
@@ -65,7 +59,7 @@ export default function Sidebar({ mobile, onClose }) {
</
div
>
</
div
>
<
div
>
<
div
>
<
h1
className=
"text-sm font-bold text-white"
>
Son of Anton
</
h1
>
<
h1
className=
"text-sm font-bold text-white"
>
Son of Anton
</
h1
>
<
p
className=
"text-[10px] text-anton-muted"
>
v4.
1
.0 — The Architect
</
p
>
<
p
className=
"text-[10px] text-anton-muted"
>
v4.
2
.0 — The Architect
</
p
>
</
div
>
</
div
>
</
div
>
</
div
>
<
button
onClick=
{
handleNew
}
className=
"w-full flex items-center justify-center gap-1.5 bg-anton-accent text-white rounded-lg py-2 text-sm hover:opacity-80 transition"
>
<
button
onClick=
{
handleNew
}
className=
"w-full flex items-center justify-center gap-1.5 bg-anton-accent text-white rounded-lg py-2 text-sm hover:opacity-80 transition"
>
...
@@ -73,7 +67,6 @@ export default function Sidebar({ mobile, onClose }) {
...
@@ -73,7 +67,6 @@ export default function Sidebar({ mobile, onClose }) {
</
button
>
</
button
>
</
div
>
</
div
>
{
/* Chat list */
}
<
div
className=
"flex-1 overflow-y-auto p-2 space-y-0.5"
>
<
div
className=
"flex-1 overflow-y-auto p-2 space-y-0.5"
>
{
state
.
chats
.
map
((
c
)
=>
(
{
state
.
chats
.
map
((
c
)
=>
(
<
div
key=
{
c
.
id
}
onClick=
{
()
=>
handleSelectChat
(
c
.
id
)
}
<
div
key=
{
c
.
id
}
onClick=
{
()
=>
handleSelectChat
(
c
.
id
)
}
...
@@ -82,8 +75,7 @@ export default function Sidebar({ mobile, onClose }) {
...
@@ -82,8 +75,7 @@ export default function Sidebar({ mobile, onClose }) {
{
editId
===
c
.
id
?
(
{
editId
===
c
.
id
?
(
<
div
className=
"flex-1 flex items-center gap-1"
>
<
div
className=
"flex-1 flex items-center gap-1"
>
<
input
value=
{
editTitle
}
onChange=
{
(
e
)
=>
setEditTitle
(
e
.
target
.
value
)
}
onKeyDown=
{
(
e
)
=>
e
.
key
===
"Enter"
&&
handleRename
(
c
.
id
)
}
<
input
value=
{
editTitle
}
onChange=
{
(
e
)
=>
setEditTitle
(
e
.
target
.
value
)
}
onKeyDown=
{
(
e
)
=>
e
.
key
===
"Enter"
&&
handleRename
(
c
.
id
)
}
className=
"flex-1 bg-anton-bg border border-anton-border rounded px-1.5 py-0.5 text-xs text-white"
autoFocus
className=
"flex-1 bg-anton-bg border border-anton-border rounded px-1.5 py-0.5 text-xs text-white"
autoFocus
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
/>
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
/>
<
button
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
handleRename
(
c
.
id
);
}
}
className=
"text-green-400"
><
Check
size=
{
12
}
/></
button
>
<
button
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
handleRename
(
c
.
id
);
}
}
className=
"text-green-400"
><
Check
size=
{
12
}
/></
button
>
<
button
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
setEditId
(
null
);
}
}
className=
"text-red-400"
><
X
size=
{
12
}
/></
button
>
<
button
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
setEditId
(
null
);
}
}
className=
"text-red-400"
><
X
size=
{
12
}
/></
button
>
</
div
>
</
div
>
...
@@ -101,21 +93,23 @@ export default function Sidebar({ mobile, onClose }) {
...
@@ -101,21 +93,23 @@ export default function Sidebar({ mobile, onClose }) {
))
}
))
}
</
div
>
</
div
>
{
/* Footer */
}
<
div
className=
"p-2 border-t border-anton-border space-y-0.5"
>
<
div
className=
"p-2 border-t border-anton-border space-y-0.5"
>
{
/* GitLab — only if superadmin OR has gitlab permission */
}
{
(
isSuperadmin
||
perms
.
can_use_gitlab
)
&&
(
<
button
onClick=
{
()
=>
nav
(
"/gitlab"
)
}
className=
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-orange-400 hover:bg-anton-card transition"
>
<
GitBranch
size=
{
14
}
/>
GitLab Center
</
button
>
)
}
{
isSuperadmin
&&
(
{
isSuperadmin
&&
(
<>
<
button
onClick=
{
()
=>
nav
(
"/admin"
)
}
className=
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-anton-muted hover:bg-anton-card hover:text-white transition"
>
<
button
onClick=
{
()
=>
nav
(
"/gitlab"
)
}
className=
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-orange-400 hover:bg-anton-card transition"
>
<
Shield
size=
{
14
}
/>
Admin
<
GitBranch
size=
{
14
}
/>
GitLab Center
</
button
>
</
button
>
)
}
<
button
onClick=
{
()
=>
nav
(
"/admin"
)
}
className=
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-anton-muted hover:bg-anton-card hover:text-white transition"
>
{
perms
.
can_use_knowledge_base
&&
(
<
Shield
size=
{
14
}
/>
Admin
<
button
onClick=
{
()
=>
nav
(
"/knowledge"
)
}
className=
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-anton-muted hover:bg-anton-card hover:text-white transition"
>
<
/
button
>
<
BookOpen
size=
{
14
}
/>
Knowledge
</>
</
button
>
)
}
)
}
<
button
onClick=
{
()
=>
nav
(
"/knowledge"
)
}
className=
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-anton-muted hover:bg-anton-card hover:text-white transition"
>
<
BookOpen
size=
{
14
}
/>
Knowledge
</
button
>
<
button
onClick=
{
()
=>
dispatch
({
type
:
"LOGOUT"
})
}
className=
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-anton-muted hover:bg-anton-card hover:text-red-400 transition"
>
<
button
onClick=
{
()
=>
dispatch
({
type
:
"LOGOUT"
})
}
className=
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-anton-muted hover:bg-anton-card hover:text-red-400 transition"
>
<
LogOut
size=
{
14
}
/>
Logout
<
LogOut
size=
{
14
}
/>
Logout
</
button
>
</
button
>
...
...
frontend/src/pages/AdminPage.jsx
View file @
738f4bcd
This diff is collapsed.
Click to expand it.
frontend/src/store.jsx
View file @
738f4bcd
import
React
,
{
createContext
,
useContext
,
useReducer
,
useCallback
}
from
"react"
;
import
React
,
{
createContext
,
useContext
,
useReducer
}
from
"react"
;
const
AppContext
=
createContext
(
null
);
const
AppContext
=
createContext
(
null
);
...
@@ -28,56 +28,23 @@ function reducer(state, action) {
...
@@ -28,56 +28,23 @@ function reducer(state, action) {
case
"SET_ACTIVE_CHAT"
:
case
"SET_ACTIVE_CHAT"
:
return
{
...
state
,
activeChatId
:
action
.
chatId
,
sidebarOpen
:
false
};
return
{
...
state
,
activeChatId
:
action
.
chatId
,
sidebarOpen
:
false
};
case
"ADD_CHAT"
:
case
"ADD_CHAT"
:
return
{
return
{
...
state
,
chats
:
[
action
.
chat
,
...
state
.
chats
],
activeChatId
:
action
.
chat
.
id
,
sidebarOpen
:
false
};
...
state
,
chats
:
[
action
.
chat
,
...
state
.
chats
],
activeChatId
:
action
.
chat
.
id
,
sidebarOpen
:
false
,
};
case
"UPDATE_CHAT"
:
case
"UPDATE_CHAT"
:
return
{
return
{
...
state
,
chats
:
state
.
chats
.
map
(
c
=>
c
.
id
===
action
.
chat
.
id
?
{
...
c
,
...
action
.
chat
}
:
c
)
};
...
state
,
chats
:
state
.
chats
.
map
((
c
)
=>
c
.
id
===
action
.
chat
.
id
?
{
...
c
,
...
action
.
chat
}
:
c
),
};
case
"REMOVE_CHAT"
:
{
case
"REMOVE_CHAT"
:
{
const
remaining
=
state
.
chats
.
filter
((
c
)
=>
c
.
id
!==
action
.
chatId
);
const
remaining
=
state
.
chats
.
filter
(
c
=>
c
.
id
!==
action
.
chatId
);
return
{
return
{
...
state
,
chats
:
remaining
,
activeChatId
:
state
.
activeChatId
===
action
.
chatId
?
remaining
[
0
]?.
id
||
null
:
state
.
activeChatId
};
...
state
,
chats
:
remaining
,
activeChatId
:
state
.
activeChatId
===
action
.
chatId
?
remaining
[
0
]?.
id
||
null
:
state
.
activeChatId
,
};
}
}
case
"SET_MESSAGES"
:
case
"SET_MESSAGES"
:
return
{
return
{
...
state
,
chatMessages
:
{
...
state
.
chatMessages
,
[
action
.
chatId
]:
action
.
messages
}
};
...
state
,
chatMessages
:
{
...
state
.
chatMessages
,
[
action
.
chatId
]:
action
.
messages
},
};
case
"ADD_MESSAGE"
:
{
case
"ADD_MESSAGE"
:
{
const
prev
=
state
.
chatMessages
[
action
.
chatId
]
||
[];
const
prev
=
state
.
chatMessages
[
action
.
chatId
]
||
[];
return
{
return
{
...
state
,
chatMessages
:
{
...
state
.
chatMessages
,
[
action
.
chatId
]:
[...
prev
,
action
.
message
]
}
};
...
state
,
chatMessages
:
{
...
state
.
chatMessages
,
[
action
.
chatId
]:
[...
prev
,
action
.
message
],
},
};
}
}
case
"SET_STREAMING"
:
case
"SET_STREAMING"
:
return
{
return
{
...
state
,
activeStreams
:
action
.
streaming
?
{
...
state
.
activeStreams
,
[
action
.
chatId
]:
true
}
:
Object
.
fromEntries
(
Object
.
entries
(
state
.
activeStreams
).
filter
(([
k
])
=>
k
!==
action
.
chatId
))
};
...
state
,
activeStreams
:
action
.
streaming
?
{
...
state
.
activeStreams
,
[
action
.
chatId
]:
true
}
:
Object
.
fromEntries
(
Object
.
entries
(
state
.
activeStreams
).
filter
(([
k
])
=>
k
!==
action
.
chatId
)
),
};
case
"SET_SIDEBAR_OPEN"
:
case
"SET_SIDEBAR_OPEN"
:
return
{
...
state
,
sidebarOpen
:
action
.
open
};
return
{
...
state
,
sidebarOpen
:
action
.
open
};
...
@@ -91,15 +58,25 @@ function reducer(state, action) {
...
@@ -91,15 +58,25 @@ function reducer(state, action) {
export
function
AppProvider
({
children
})
{
export
function
AppProvider
({
children
})
{
const
[
state
,
dispatch
]
=
useReducer
(
reducer
,
initialState
);
const
[
state
,
dispatch
]
=
useReducer
(
reducer
,
initialState
);
return
(
return
<
AppContext
.
Provider
value=
{
{
state
,
dispatch
}
}
>
{
children
}
</
AppContext
.
Provider
>;
<
AppContext
.
Provider
value=
{
{
state
,
dispatch
}
}
>
{
children
}
</
AppContext
.
Provider
>
);
}
}
export
function
useApp
()
{
export
function
useApp
()
{
const
ctx
=
useContext
(
AppContext
);
const
ctx
=
useContext
(
AppContext
);
if
(
!
ctx
)
throw
new
Error
(
"useApp must be inside AppProvider"
);
if
(
!
ctx
)
throw
new
Error
(
"useApp must be inside AppProvider"
);
return
ctx
;
return
ctx
;
}
/** Helper to get current user permissions with sane defaults */
export
function
usePermissions
()
{
const
{
state
}
=
useApp
();
const
p
=
state
.
user
?.
permissions
;
if
(
!
p
)
return
{
can_use_web_search
:
false
,
can_use_ui_design
:
false
,
can_use_knowledge_base
:
true
,
can_use_gitlab
:
false
,
can_use_attachments
:
true
,
can_export_pptx
:
true
,
can_export_docx
:
true
,
allowed_models
:
"all"
,
max_tokens_cap
:
4096
,
max_reasoning_budget
:
0
,
max_chats
:
50
,
max_messages_per_day
:
100
,
max_knowledge_bases
:
3
,
max_documents_per_kb
:
20
,
max_attachment_size_mb
:
10
,
max_attachments_per_message
:
5
,
};
return
p
;
}
}
\ 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