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
49192084
Commit
49192084
authored
Apr 10, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 9 files via Son of Anton
parent
f96d6514
Changes
9
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
647 additions
and
488 deletions
+647
-488
config.py
backend/config.py
+5
-0
main.py
backend/main.py
+5
-1
models.py
backend/models.py
+8
-3
admin_routes.py
backend/routes/admin_routes.py
+37
-3
auth_routes.py
backend/routes/auth_routes.py
+19
-3
seed.py
backend/seed.py
+15
-6
api.js
frontend/src/api.js
+232
-97
AdminPage.jsx
frontend/src/pages/AdminPage.jsx
+230
-293
LoginPage.jsx
frontend/src/pages/LoginPage.jsx
+96
-82
No files found.
backend/config.py
View file @
49192084
...
...
@@ -58,6 +58,11 @@ CHROMADB_PATH = os.getenv("CHROMADB_PATH", "/data/chromadb")
# ═══════════════════════════════════════════════════
SERPAPI_KEY
=
os
.
getenv
(
"SERPAPI_KEY"
,
""
)
# ═══════════════════════════════════════════════════
# App-level defaults
# ═══════════════════════════════════════════════════
REGISTRATION_ENABLED_DEFAULT
=
os
.
getenv
(
"REGISTRATION_ENABLED"
,
"true"
)
.
lower
()
in
(
"true"
,
"1"
,
"yes"
)
# ═══════════════════════════════════════════════════
# PERMISSION DEFAULTS — applied to new regular users
# ═══════════════════════════════════════════════════
...
...
backend/main.py
View file @
49192084
...
...
@@ -48,12 +48,16 @@ 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"
)
if
"app_settings"
not
in
existing_tables
:
from
backend.models
import
AppSettings
AppSettings
.
__table__
.
create
(
bind
=
engine
,
checkfirst
=
True
)
print
(
" Created app_settings table"
)
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 @
49192084
...
...
@@ -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,14 @@ class UserPermissions(Base):
user
=
relationship
(
"User"
,
back_populates
=
"permissions"
)
class
AppSettings
(
Base
):
__tablename__
=
"app_settings"
id
=
Column
(
String
(
36
),
primary_key
=
True
,
default
=
new_id
)
registration_enabled
=
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 @
49192084
"""
Superadmin routes: user management, stats, permissions — v4.2.0
Superadmin routes: user management, stats, permissions
, app settings
— v4.2.0
"""
from
pydantic
import
BaseModel
...
...
@@ -10,7 +10,7 @@ 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.models
import
User
,
Chat
,
Message
,
KnowledgeBase
,
UserPermissions
,
AppSettings
from
backend.auth
import
(
require_superadmin
,
hash_password
,
get_user_permissions
,
ensure_user_permissions
,
get_default_permissions_template
,
...
...
@@ -55,6 +55,10 @@ class PermissionsBody(BaseModel):
max_attachments_per_message
:
Optional
[
int
]
=
None
class
AppSettingsBody
(
BaseModel
):
registration_enabled
:
Optional
[
bool
]
=
None
# ═══════════════════════════════════════════════════
# Stats & Users
# ═══════════════════════════════════════════════════
...
...
@@ -107,7 +111,6 @@ def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin),
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
}
...
...
@@ -160,6 +163,37 @@ def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depe
return
result
# ═══════════════════════════════════════════════════
# APP SETTINGS (registration toggle, etc.)
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/app-settings"
)
def
get_app_settings
(
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
settings
=
db
.
query
(
AppSettings
)
.
first
()
if
not
settings
:
return
{
"registration_enabled"
:
True
}
return
{
"registration_enabled"
:
settings
.
registration_enabled
,
"updated_at"
:
str
(
settings
.
updated_at
)
if
settings
.
updated_at
else
None
,
}
@
router
.
put
(
"/app-settings"
)
def
update_app_settings
(
body
:
AppSettingsBody
,
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
settings
=
db
.
query
(
AppSettings
)
.
first
()
if
not
settings
:
settings
=
AppSettings
()
db
.
add
(
settings
)
if
body
.
registration_enabled
is
not
None
:
settings
.
registration_enabled
=
body
.
registration_enabled
db
.
commit
()
db
.
refresh
(
settings
)
return
{
"registration_enabled"
:
settings
.
registration_enabled
,
"updated_at"
:
str
(
settings
.
updated_at
)
if
settings
.
updated_at
else
None
,
}
# ═══════════════════════════════════════════════════
# PERMISSIONS MANAGEMENT
# ═══════════════════════════════════════════════════
...
...
backend/routes/auth_routes.py
View file @
49192084
"""
Authentication routes: register, login, profile — with permissions.
Authentication routes: register, login, profile — with permissions
and 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
(
"/registration-status"
)
def
registration_status
(
db
:
Session
=
Depends
(
get_db
)):
"""Public endpoint — no auth required. Frontend checks this to show/hide register form."""
settings
=
db
.
query
(
AppSettings
)
.
first
()
enabled
=
settings
.
registration_enabled
if
settings
else
config
.
REGISTRATION_ENABLED_DEFAULT
return
{
"registration_enabled"
:
enabled
}
@
router
.
post
(
"/register"
)
def
register
(
body
:
RegisterBody
,
db
:
Session
=
Depends
(
get_db
)):
# Check if registration is enabled
settings
=
db
.
query
(
AppSettings
)
.
first
()
enabled
=
settings
.
registration_enabled
if
settings
else
config
.
REGISTRATION_ENABLED_DEFAULT
if
not
enabled
:
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Registration is currently disabled. Contact your 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
)
...
...
backend/seed.py
View file @
49192084
"""
Seed superadmin user
and default permissions template
.
Seed superadmin user
, default permissions template, and app settings
.
"""
from
backend.database
import
SessionLocal
from
backend.models
import
User
,
UserPermissions
from
backend.models
import
User
,
UserPermissions
,
AppSettings
from
backend.auth
import
hash_password
from
backend.config
import
SUPERADMIN_PASSWORD
,
SUPERADMIN_PERMISSIONS
,
DEFAULT_PERMISSIONS
,
PERMISSION_FIELDS
from
backend.config
import
(
SUPERADMIN_PASSWORD
,
SUPERADMIN_PERMISSIONS
,
DEFAULT_PERMISSIONS
,
PERMISSION_FIELDS
,
REGISTRATION_ENABLED_DEFAULT
,
)
def
seed_superadmin
():
...
...
@@ -25,7 +28,6 @@ def seed_superadmin():
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
):
...
...
@@ -34,7 +36,6 @@ def seed_superadmin():
db
.
commit
()
print
(
" Created superadmin permissions"
)
else
:
# 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
)
...
...
@@ -45,7 +46,7 @@ def seed_superadmin():
db
.
commit
()
print
(
" Created superadmin permissions (existing user)"
)
# Create/update defaults template
(special row with user_id = "__defaults__")
# Create/update defaults template
defaults
=
db
.
query
(
UserPermissions
)
.
filter
(
UserPermissions
.
user_id
==
"__defaults__"
)
.
first
()
if
not
defaults
:
defaults
=
UserPermissions
(
user_id
=
"__defaults__"
)
...
...
@@ -56,6 +57,14 @@ def seed_superadmin():
db
.
commit
()
print
(
" Created default permissions template"
)
# Seed app settings if missing
app_settings
=
db
.
query
(
AppSettings
)
.
first
()
if
not
app_settings
:
app_settings
=
AppSettings
(
registration_enabled
=
REGISTRATION_ENABLED_DEFAULT
)
db
.
add
(
app_settings
)
db
.
commit
()
print
(
f
" Created app settings (registration: {REGISTRATION_ENABLED_DEFAULT})"
)
except
Exception
as
e
:
print
(
f
" Seed error: {e}"
)
finally
:
...
...
frontend/src/api.js
View file @
49192084
This diff is collapsed.
Click to expand it.
frontend/src/pages/AdminPage.jsx
View file @
49192084
This diff is collapsed.
Click to expand it.
frontend/src/pages/LoginPage.jsx
View file @
49192084
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
,
getRegistrationStatus
}
from
"../api"
;
import
{
Flame
,
LogIn
,
UserPlus
,
Eye
,
EyeOff
,
AlertCircle
}
from
"lucide-react"
;
export
default
function
LoginPage
()
{
const
{
dispatch
}
=
useApp
();
const
[
is
Register
,
setIsRegister
]
=
useState
(
fals
e
);
const
[
is
Login
,
setIsLogin
]
=
useState
(
tru
e
);
const
[
username
,
setUsername
]
=
useState
(
""
);
const
[
email
,
setEmail
]
=
useState
(
""
);
const
[
password
,
setPassword
]
=
useState
(
""
);
const
[
showP
w
,
setShowPw
]
=
useState
(
false
);
const
[
showP
assword
,
setShowPassword
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
registrationEnabled
,
setRegistrationEnabled
]
=
useState
(
true
);
const
[
checkingRegistration
,
setCheckingRegistration
]
=
useState
(
true
);
useEffect
(()
=>
{
(
async
()
=>
{
try
{
const
data
=
await
getRegistrationStatus
();
setRegistrationEnabled
(
data
.
registration_enabled
);
}
catch
{
setRegistrationEnabled
(
true
);
}
finally
{
setCheckingRegistration
(
false
);
}
})();
},
[]);
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
data
;
if
(
isLogin
)
{
data
=
await
login
(
username
,
password
);
}
else
{
data
=
await
register
(
username
,
email
,
password
);
}
dispatch
({
type
:
"LOGIN"
,
token
:
data
.
token
,
user
:
data
.
user
});
}
catch
(
err
)
{
setError
(
err
.
message
||
"
Authentication failed
"
);
setError
(
err
.
message
||
"
Something went wrong
"
);
}
finally
{
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=
"w-full max-w-sm"
>
{
/* Logo */
}
<
div
className=
"min-h-screen bg-anton-bg flex items-center justify-center p-4"
>
<
div
className=
"w-full max-w-md"
>
<
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"
>
<
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
>
<
h1
className=
"text-3xl font-bold text-white"
>
Son of Anton
</
h1
>
<
p
className=
"text-anton-muted mt-1 text-sm"
>
Avatar of All Elements of Code
</
p
>
</
div
>
<
div
className=
"bg-anton-card border border-anton-border rounded-2xl p-6 shadow-xl"
>
{
!
checkingRegistration
&&
registrationEnabled
&&
(
<
div
className=
"flex mb-6 bg-anton-bg rounded-xl p-1"
>
<
button
onClick=
{
()
=>
{
setIsLogin
(
true
);
setError
(
""
);
}
}
className=
{
`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition ${isLogin ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"}`
}
>
Sign In
</
button
>
<
button
onClick=
{
()
=>
{
setIsLogin
(
false
);
setError
(
""
);
}
}
className=
{
`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition ${!isLogin ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"}`
}
>
Register
</
button
>
</
div
>
)
}
{
error
&&
(
<
div
className=
"mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-2 text-red-400 text-sm"
>
<
AlertCircle
size=
{
16
}
className=
"shrink-0"
/>
{
error
}
</
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"
/>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Username
</
label
>
<
input
type=
"text"
value=
{
username
}
onChange=
{
(
e
)
=>
setUsername
(
e
.
target
.
value
)
}
placeholder=
"Enter username"
required
autoFocus
className=
"w-full bg-anton-bg border border-anton-border rounded-xl px-4 py-3 text-white text-sm focus:outline-none focus:border-anton-accent transition"
/>
</
div
>
{
isRegister
&&
(
{
!
isLogin
&&
(
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1.5 block"
>
Email
</
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"
/>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Email
</
label
>
<
input
type=
"email"
value=
{
email
}
onChange=
{
(
e
)
=>
setEmail
(
e
.
target
.
value
)
}
placeholder=
"Enter email"
required
className=
"w-full bg-anton-bg border border-anton-border rounded-xl px-4 py-3 text-white text-sm focus:outline-none focus:border-anton-accent transition"
/>
</
div
>
)
}
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1.5
block"
>
Password
</
label
>
<
label
className=
"text-xs text-anton-muted mb-1
block"
>
Password
</
label
>
<
div
className=
"relative"
>
<
input
type=
{
showPw
?
"text"
:
"password"
}
value=
{
password
}
onChange=
{
(
e
)
=>
setPassword
(
e
.
target
.
value
)
}
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"
}
/>
<
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
}
/>
}
<
input
type=
{
showPassword
?
"text"
:
"password"
}
value=
{
password
}
onChange=
{
(
e
)
=>
setPassword
(
e
.
target
.
value
)
}
placeholder=
"Enter password"
required
className=
"w-full bg-anton-bg border border-anton-border rounded-xl px-4 py-3 pr-10 text-white text-sm focus:outline-none focus:border-anton-accent transition"
/>
<
button
type=
"button"
onClick=
{
()
=>
setShowPassword
(
!
showPassword
)
}
className=
"absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition"
>
{
showPassword
?
<
EyeOff
size=
{
16
}
/>
:
<
Eye
size=
{
16
}
/>
}
</
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 bg-anton-accent text-white rounded-xl py-3 font-medium hover:opacity-90 transition disabled:opacity-50 flex items-center justify-center gap-2"
>
{
loading
?
(
<
span
className=
"w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"
/>
)
:
isLogin
?
(
<><
LogIn
size=
{
18
}
/>
Sign In
</>
)
:
(
<><
UserPlus
size=
{
18
}
/>
Create Account
</>
)
}
<
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=
"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
>
{
!
checkingRegistration
&&
!
registrationEnabled
&&
(
<
div
className=
"mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg text-yellow-400 text-xs text-center"
>
Registration is currently disabled. Contact your administrator.
</
div
>
)
}
</
div
>
<
p
className=
"text-center text-anton-muted text-xs mt-6"
>
Created by Mahmoud Aglan — AL-Arcade
</
p
>
</
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