Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
Son Of Anton
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Son Of Anton
Commits
705bdcba
Commit
705bdcba
authored
Apr 10, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 5 files via Son of Anton
parent
e3643210
Changes
5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
720 additions
and
414 deletions
+720
-414
admin_routes.py
backend/routes/admin_routes.py
+251
-12
auth_routes.py
backend/routes/auth_routes.py
+21
-11
api.js
frontend/src/api.js
+163
-7
AdminPage.jsx
frontend/src/pages/AdminPage.jsx
+211
-297
LoginPage.jsx
frontend/src/pages/LoginPage.jsx
+74
-87
No files found.
backend/routes/admin_routes.py
View file @
705bdcba
This diff is collapsed.
Click to expand it.
backend/routes/auth_routes.py
View file @
705bdcba
"""
Authentication routes: register, login, profile — with permissions
+
registration toggle.
Authentication routes: register, login, profile — with permissions
and
registration toggle.
"""
from
pydantic
import
BaseModel
...
...
@@ -28,23 +28,22 @@ class LoginBody(BaseModel):
password
:
str
@
router
.
get
(
"/config"
)
def
auth_config
(
db
:
Session
=
Depends
(
get_db
)):
"""Public endpoint — no auth needed. Returns registration status."""
settings
=
db
.
query
(
AppSettings
)
.
first
()
return
{
"allow_registration"
:
settings
.
allow_registration
if
settings
else
True
,
}
class
ProfileOut
(
BaseModel
):
pass
class
Config
(
BaseModel
):
pass
@
router
.
post
(
"/register"
)
def
register
(
body
:
RegisterBody
,
db
:
Session
=
Depends
(
get_db
)):
# Check if registration is enabled
app_
settings
=
db
.
query
(
AppSettings
)
.
first
()
if
app_settings
and
not
app_
settings
.
allow_registration
:
settings
=
db
.
query
(
AppSettings
)
.
first
()
if
settings
and
not
settings
.
allow_registration
:
raise
HTTPException
(
status
.
HTTP_403_FORBIDDEN
,
"Registration is currently disabled. Contact
an
administrator."
,
"Registration is currently disabled. Contact
your
administrator."
,
)
if
db
.
query
(
User
)
.
filter
(
...
...
@@ -63,6 +62,7 @@ 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
)
...
...
@@ -88,6 +88,16 @@ def me(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
return
_user_dict
(
user
,
perms
)
@
router
.
get
(
"/config"
)
def
get_public_config
(
db
:
Session
=
Depends
(
get_db
)):
"""Public endpoint — no auth required. Returns app config the login page needs."""
settings
=
db
.
query
(
AppSettings
)
.
first
()
allow_reg
=
True
if
settings
:
allow_reg
=
settings
.
allow_registration
return
{
"allow_registration"
:
allow_reg
}
def
_user_dict
(
u
:
User
,
perms
:
dict
=
None
)
->
dict
:
d
=
{
"id"
:
u
.
id
,
...
...
frontend/src/api.js
View file @
705bdcba
// ── Registration toggle (append to end of file) ──
const
BASE
=
"/api"
;
export
const
getAuthConfig
=
()
=>
fetch
(
`
${
BASE
}
/auth/config`
).
then
((
r
)
=>
r
.
json
());
function
headers
(
token
)
{
const
h
=
{
"Content-Type"
:
"application/json"
};
if
(
token
)
h
[
"Authorization"
]
=
`Bearer
${
token
}
`
;
return
h
;
}
export
const
getRegistrationSetting
=
(
token
)
=>
request
(
"GET"
,
"/admin/registration"
,
token
);
function
authHeader
(
token
)
{
return
token
?
{
Authorization
:
`Bearer
${
token
}
`
}
:
{};
}
export
const
toggleRegistration
=
(
token
)
=>
request
(
"PUT"
,
"/admin/registration"
,
token
);
\ No newline at end of file
async
function
request
(
method
,
path
,
token
,
body
)
{
const
opts
=
{
method
,
headers
:
headers
(
token
)
};
if
(
body
)
opts
.
body
=
JSON
.
stringify
(
body
);
const
res
=
await
fetch
(
`
${
BASE
}${
path
}
`
,
opts
);
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
err
.
detail
||
err
.
message
||
"Request failed"
);
}
return
res
.
json
();
}
export
const
login
=
(
username
,
password
)
=>
request
(
"POST"
,
"/auth/login"
,
null
,
{
username
,
password
});
export
const
register
=
(
username
,
email
,
password
)
=>
request
(
"POST"
,
"/auth/register"
,
null
,
{
username
,
email
,
password
});
export
const
getMe
=
(
token
)
=>
request
(
"GET"
,
"/auth/me"
,
token
);
export
const
getPublicConfig
=
()
=>
request
(
"GET"
,
"/auth/config"
,
null
);
export
const
listChats
=
(
token
)
=>
request
(
"GET"
,
"/chats"
,
token
);
export
const
createChat
=
(
token
,
data
=
{})
=>
request
(
"POST"
,
"/chats"
,
token
,
data
);
export
const
updateChat
=
(
token
,
chatId
,
data
)
=>
request
(
"PUT"
,
`/chats/
${
chatId
}
`
,
token
,
data
);
export
const
renameChat
=
(
token
,
chatId
,
title
)
=>
updateChat
(
token
,
chatId
,
{
title
});
export
const
deleteChat
=
(
token
,
chatId
)
=>
request
(
"DELETE"
,
`/chats/
${
chatId
}
`
,
token
);
export
const
getMessages
=
(
token
,
chatId
)
=>
request
(
"GET"
,
`/chats/
${
chatId
}
/messages`
,
token
);
export
async
function
*
streamMessage
(
token
,
chatId
,
body
,
signal
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/messages`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
(
body
),
signal
,
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
err
.
detail
||
"Stream failed"
);
}
const
reader
=
res
.
body
.
getReader
();
const
decoder
=
new
TextDecoder
();
let
buffer
=
""
;
while
(
true
)
{
const
{
done
,
value
}
=
await
reader
.
read
();
if
(
done
)
break
;
buffer
+=
decoder
.
decode
(
value
,
{
stream
:
true
});
const
parts
=
buffer
.
split
(
"
\n\n
"
);
buffer
=
parts
.
pop
()
||
""
;
for
(
const
part
of
parts
)
{
const
line
=
part
.
trim
();
if
(
line
.
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
line
.
slice
(
6
));
}
catch
{
}
}
}
}
if
(
buffer
.
trim
().
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
buffer
.
trim
().
slice
(
6
));
}
catch
{
}
}
}
export
async
function
uploadAttachments
(
token
,
chatId
,
files
)
{
const
form
=
new
FormData
();
for
(
const
file
of
files
)
form
.
append
(
"files"
,
file
);
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/attachments`
,
{
method
:
"POST"
,
headers
:
authHeader
(
token
),
body
:
form
,
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
err
.
detail
||
"Upload failed"
);
}
return
res
.
json
();
}
export
function
getAttachmentUrl
(
attachmentId
)
{
return
`
${
BASE
}
/attachments/
${
attachmentId
}
/file`
;
}
export
const
deleteAttachment
=
(
token
,
attachmentId
)
=>
request
(
"DELETE"
,
`/attachments/
${
attachmentId
}
`
,
token
);
export
const
listKnowledgeBases
=
(
token
)
=>
request
(
"GET"
,
"/knowledge"
,
token
);
export
const
createKnowledgeBase
=
(
token
,
name
,
description
=
""
)
=>
request
(
"POST"
,
"/knowledge"
,
token
,
{
name
,
description
});
export
const
getKnowledgeBase
=
(
token
,
kbId
)
=>
request
(
"GET"
,
`/knowledge/
${
kbId
}
`
,
token
);
export
const
deleteKnowledgeBase
=
(
token
,
kbId
)
=>
request
(
"DELETE"
,
`/knowledge/
${
kbId
}
`
,
token
);
export
async
function
uploadDocuments
(
token
,
kbId
,
files
)
{
const
form
=
new
FormData
();
for
(
const
file
of
files
)
form
.
append
(
"files"
,
file
);
const
res
=
await
fetch
(
`
${
BASE
}
/knowledge/
${
kbId
}
/upload`
,
{
method
:
"POST"
,
headers
:
authHeader
(
token
),
body
:
form
,
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
err
.
detail
||
"Upload failed"
);
}
return
res
.
json
();
}
export
const
uploadDocument
=
(
token
,
kbId
,
file
)
=>
uploadDocuments
(
token
,
kbId
,
[
file
]);
export
const
adminStats
=
(
token
)
=>
request
(
"GET"
,
"/admin/stats"
,
token
);
export
const
adminListUsers
=
(
token
)
=>
request
(
"GET"
,
"/admin/users"
,
token
);
export
const
adminCreateUser
=
(
token
,
data
)
=>
request
(
"POST"
,
"/admin/users"
,
token
,
data
);
export
const
adminUpdateUser
=
(
token
,
userId
,
data
)
=>
request
(
"PUT"
,
`/admin/users/
${
userId
}
`
,
token
,
data
);
export
const
adminDeleteUser
=
(
token
,
userId
)
=>
request
(
"DELETE"
,
`/admin/users/
${
userId
}
`
,
token
);
export
const
adminListChats
=
(
token
)
=>
request
(
"GET"
,
"/admin/chats"
,
token
);
export
const
adminGetSettings
=
(
token
)
=>
request
(
"GET"
,
"/admin/settings"
,
token
);
export
const
adminUpdateSettings
=
(
token
,
data
)
=>
request
(
"PUT"
,
"/admin/settings"
,
token
,
data
);
export
async
function
downloadZip
(
token
,
markdown
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/files/download-zip`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
}),
});
if
(
!
res
.
ok
)
throw
new
Error
(
"Download failed"
);
const
ct
=
res
.
headers
.
get
(
"content-type"
)
||
""
;
if
(
ct
.
includes
(
"application/zip"
))
{
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
a
.
download
=
"son-of-anton-code.zip"
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
else
{
const
data
=
await
res
.
json
();
if
(
data
.
error
)
throw
new
Error
(
data
.
error
);
}
}
\ No newline at end of file
frontend/src/pages/AdminPage.jsx
View file @
705bdcba
This diff is collapsed.
Click to expand it.
frontend/src/pages/LoginPage.jsx
View file @
705bdcba
import
React
,
{
useState
,
useEffect
}
from
"react"
;
import
{
useApp
}
from
"../store"
;
import
{
login
,
register
,
get
Auth
Config
}
from
"../api"
;
import
{
Flame
,
LogIn
,
UserPlus
,
AlertCircle
}
from
"lucide-react"
;
import
{
login
,
register
,
get
Public
Config
}
from
"../api"
;
import
{
Flame
,
Eye
,
EyeOff
,
Loader2
}
from
"lucide-react"
;
export
default
function
LoginPage
()
{
const
{
dispatch
}
=
useApp
();
...
...
@@ -9,21 +9,23 @@ export default function LoginPage() {
const
[
username
,
setUsername
]
=
useState
(
""
);
const
[
email
,
setEmail
]
=
useState
(
""
);
const
[
password
,
setPassword
]
=
useState
(
""
);
const
[
showPw
,
setShowPw
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
allowRegistration
,
setAllowRegistration
]
=
useState
(
true
);
const
[
configLoaded
,
setConfigLoaded
]
=
useState
(
false
);
useEffect
(()
=>
{
getAuthConfig
()
.
then
((
data
)
=>
{
setAllowRegistration
(
data
.
allow_registration
!==
false
);
set
ConfigLoaded
(
true
);
}
)
.
catch
(()
=>
{
(
async
()
=>
{
try
{
const
cfg
=
await
getPublicConfig
(
);
set
AllowRegistration
(
cfg
.
allow_registration
);
}
catch
{
// If config fetch fails, default to allowing registration
setAllowRegistration
(
true
);
setConfigLoaded
(
true
);
});
}
setConfigLoaded
(
true
);
})();
},
[]);
async
function
handleSubmit
(
e
)
{
...
...
@@ -31,26 +33,31 @@ export default function LoginPage() {
setError
(
""
);
setLoading
(
true
);
try
{
let
res
ult
;
let
res
;
if
(
isRegister
)
{
if
(
!
allowRegistration
)
{
setError
(
"Registration is disabled."
);
setLoading
(
false
);
return
;
}
result
=
await
register
(
username
,
email
,
password
);
if
(
!
email
)
{
setError
(
"Email is required"
);
setLoading
(
false
);
return
;
}
res
=
await
register
(
username
,
email
,
password
);
}
else
{
res
ult
=
await
login
(
username
,
password
);
res
=
await
login
(
username
,
password
);
}
dispatch
({
type
:
"LOGIN"
,
token
:
result
.
token
,
user
:
result
.
user
});
dispatch
({
type
:
"SET_TOKEN"
,
token
:
res
.
token
});
dispatch
({
type
:
"SET_USER"
,
user
:
res
.
user
});
}
catch
(
err
)
{
setError
(
err
.
message
||
"Something went wrong"
);
setError
(
err
.
message
);
}
setLoading
(
false
);
}
if
(
!
configLoaded
)
{
return
(
<
div
className=
"h-dvh flex items-center justify-center bg-anton-bg"
>
<
Loader2
className=
"animate-spin text-anton-accent"
size=
{
32
}
/>
</
div
>
);
}
return
(
<
div
className=
"h-dvh flex items-center justify-center bg-anton-bg p-4"
>
<
div
className=
"h-dvh flex items-center justify-center bg-anton-bg p
x
-4"
>
<
div
className=
"w-full max-w-sm"
>
<
div
className=
"flex flex-col items-center mb-8"
>
<
div
className=
"w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20 mb-4"
>
...
...
@@ -60,85 +67,65 @@ export default function LoginPage() {
<
p
className=
"text-anton-muted text-sm mt-1"
>
Avatar of All Elements of Code
</
p
>
</
div
>
<
div
className=
"bg-anton-card border border-anton-border rounded-2xl p-6"
>
{
/* Tab buttons — only show Register tab if registration is allowed */
}
<
div
className=
"flex gap-2 mb-6"
>
<
button
onClick=
{
()
=>
{
setIsRegister
(
false
);
setError
(
""
);
}
}
className=
{
`flex-1 py-2 rounded-lg text-sm font-medium transition ${
!isRegister ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"
}`
}
>
<
LogIn
size=
{
14
}
className=
"inline mr-1.5"
/>
Login
</
button
>
{
allowRegistration
&&
(
<
button
onClick=
{
()
=>
{
setIsRegister
(
true
);
setError
(
""
);
}
}
className=
{
`flex-1 py-2 rounded-lg text-sm font-medium transition ${
isRegister ? "bg-anton-accent text-white" : "text-anton-muted hover:text-white"
}`
}
>
<
UserPlus
size=
{
14
}
className=
"inline mr-1.5"
/>
Register
</
button
>
)
}
</
div
>
<
form
onSubmit=
{
handleSubmit
}
className=
"bg-anton-card border border-anton-border rounded-2xl p-6 space-y-4"
>
<
h2
className=
"text-lg font-semibold text-white text-center"
>
{
isRegister
?
"Create Account"
:
"Sign In"
}
</
h2
>
{
error
&&
(
<
div
className=
"mb-4 bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 flex items-start gap-2"
>
<
AlertCircle
size=
{
14
}
className=
"text-red-400 mt-0.5 shrink-0"
/>
<
span
className=
"text-red-400 text-xs"
>
{
error
}
</
span
>
<
div
className=
"bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 text-red-400 text-sm"
>
{
error
}
</
div
>
)
}
<
form
onSubmit=
{
handleSubmit
}
className=
"space-y-4"
>
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Username
</
label
>
<
input
type=
"text"
value=
{
username
}
onChange=
{
(
e
)
=>
setUsername
(
e
.
target
.
value
)
}
required
autoFocus
className=
"w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
</
div
>
{
isRegister
&&
(
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Username
</
label
>
<
input
type=
"text"
value=
{
username
}
onChange=
{
(
e
)
=>
setUsername
(
e
.
target
.
value
)
}
className=
"w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent transition"
required
autoComplete=
"username"
/>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Email
</
label
>
<
input
type=
"email"
value=
{
email
}
onChange=
{
(
e
)
=>
setEmail
(
e
.
target
.
value
)
}
required
className=
"w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent"
/>
</
div
>
)
}
{
isRegister
&&
(
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Email
</
label
>
<
input
type=
"email"
value=
{
email
}
onChange=
{
(
e
)
=>
setEmail
(
e
.
target
.
value
)
}
className=
"w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent transition"
required
autoComplete=
"email"
/>
</
div
>
)
}
<
div
>
<
label
className=
"text-xs text-anton-muted mb-1 block"
>
Password
</
label
>
<
input
type=
"password"
value=
{
password
}
onChange=
{
(
e
)
=>
setPassword
(
e
.
target
.
value
)
}
className=
"w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent transition"
required
autoComplete=
{
isRegister
?
"new-password"
:
"current-password"
}
/>
<
div
>
<
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
)
}
required
className=
"w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-anton-accent pr-10"
/>
<
button
type=
"button"
onClick=
{
()
=>
setShowPw
(
!
showPw
)
}
className=
"absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white"
>
{
showPw
?
<
EyeOff
size=
{
16
}
/>
:
<
Eye
size=
{
16
}
/>
}
</
button
>
</
div
>
</
div
>
<
button
type=
"submit"
disabled=
{
loading
}
className=
"w-full bg-anton-accent text-white py-2.5 rounded-lg font-medium hover:opacity-90 transition disabled:opacity-50"
>
{
loading
?
"..."
:
isRegister
?
"Create Account"
:
"Sign In"
}
</
button
>
</
form
>
<
button
type=
"submit"
disabled=
{
loading
}
className=
"w-full bg-anton-accent text-white rounded-lg py-2.5 text-sm font-medium hover:opacity-90 transition disabled:opacity-50 flex items-center justify-center gap-2"
>
{
loading
&&
<
Loader2
size=
{
16
}
className=
"animate-spin"
/>
}
{
isRegister
?
"Create Account"
:
"Sign In"
}
</
button
>
{
!
allowRegistration
&&
configLoaded
&&
!
isRegister
&&
(
<
p
className=
"text-[11px] text-anton-muted text-center mt-4"
>
Registration is currently disabled by the administrator.
{
allowRegistration
&&
(
<
p
className=
"text-center text-sm text-anton-muted"
>
{
isRegister
?
"Already have an account?"
:
"Don't have an account?"
}{
" "
}
<
button
type=
"button"
onClick=
{
()
=>
{
setIsRegister
(
!
isRegister
);
setError
(
""
);
}
}
className=
"text-anton-accent hover:underline"
>
{
isRegister
?
"Sign In"
:
"Register"
}
</
button
>
</
p
>
)
}
</
div
>
{
!
allowRegistration
&&
!
isRegister
&&
(
<
p
className=
"text-center text-xs text-anton-muted"
>
Registration is disabled. Contact your administrator.
</
p
>
)
}
</
form
>
</
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