mirror of
https://github.com/markbeep/AudioBookRequest.git
synced 2026-04-25 01:48:17 -05:00
add admin init page
This commit is contained in:
+7
-1
@@ -20,12 +20,18 @@ async def redirect_to_init(request: Request, call_next: Any):
|
||||
Initial redirect if no user exists. We force the user to create a new login
|
||||
"""
|
||||
global user_exists
|
||||
if not user_exists and request.url.path not in ["/init", "/globals.css"]:
|
||||
if (
|
||||
not user_exists
|
||||
and request.url.path not in ["/init", "/globals.css"]
|
||||
and request.method == "GET"
|
||||
):
|
||||
session = next(get_session())
|
||||
user_count = session.exec(select(func.count()).select_from(User)).one()
|
||||
if user_count == 0:
|
||||
return RedirectResponse("/init")
|
||||
else:
|
||||
user_exists = True
|
||||
elif user_exists and request.url.path.startswith("/init"):
|
||||
return RedirectResponse("/")
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
+2
-2
@@ -7,5 +7,5 @@ class BaseModel(SQLModel):
|
||||
|
||||
|
||||
class User(BaseModel, table=True):
|
||||
username: bytes = Field(primary_key=True)
|
||||
password: bytes
|
||||
username: str = Field(primary_key=True)
|
||||
password: str
|
||||
|
||||
+34
-2
@@ -1,9 +1,13 @@
|
||||
import re
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import FileResponse
|
||||
from jinja2_fragments.fastapi import Jinja2Blocks
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.util.auth import get_username
|
||||
from app.db import get_session
|
||||
|
||||
from app.util.auth import create_user, get_username
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -25,3 +29,31 @@ def read_root(request: Request, username: Annotated[str, Depends(get_username)])
|
||||
@router.get("/init")
|
||||
def read_init(request: Request):
|
||||
return templates.TemplateResponse("init.html", {"request": request})
|
||||
|
||||
|
||||
validate_password_regex = re.compile(
|
||||
r"^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d).{8,}$"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/init", status_code=201)
|
||||
def create_init(
|
||||
username: Annotated[str, Form()],
|
||||
password: Annotated[str, Form()],
|
||||
confirm_password: Annotated[str, Form()],
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
):
|
||||
if password != confirm_password:
|
||||
raise HTTPException(status_code=400, detail="Passwords do not match")
|
||||
|
||||
print(username, password, confirm_password, validate_password_regex.match(password))
|
||||
|
||||
if not validate_password_regex.match(password):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number",
|
||||
)
|
||||
|
||||
user = create_user(username, password)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
+19
-10
@@ -1,24 +1,29 @@
|
||||
from typing import Annotated
|
||||
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
import secrets
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.db import get_session
|
||||
from app.models import User
|
||||
|
||||
security = HTTPBasic()
|
||||
ph = PasswordHasher()
|
||||
|
||||
|
||||
def create_user(username: str, password: str) -> User:
|
||||
password_hash = ph.hash(password)
|
||||
return User(username=username, password=password_hash)
|
||||
|
||||
|
||||
def get_username(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
||||
):
|
||||
username_bytes = credentials.username.encode("utf-8")
|
||||
password_bytes = credentials.password.encode("utf-8")
|
||||
|
||||
user = session.exec(
|
||||
select(User).where(User.username == username_bytes)
|
||||
select(User).where(User.username == credentials.username)
|
||||
).one_or_none()
|
||||
|
||||
if not user:
|
||||
@@ -28,14 +33,18 @@ def get_username(
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
is_correct_username = secrets.compare_digest(user.username, username_bytes)
|
||||
is_correct_password = secrets.compare_digest(user.password, password_bytes)
|
||||
|
||||
if not (is_correct_username and is_correct_password):
|
||||
try:
|
||||
ph.verify(user.password, credentials.password)
|
||||
except VerifyMismatchError:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid credentials",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
if ph.check_needs_rehash(user.password):
|
||||
user.password = ph.hash(credentials.password)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
return credentials.username
|
||||
|
||||
@@ -46,3 +46,4 @@ aiohttp==3.11.12
|
||||
alembic==1.14.1
|
||||
jinja2-fragments==1.7.0
|
||||
pytailwindcss==0.2.0
|
||||
argon2-cffi==23.1.0
|
||||
|
||||
+83
-7
@@ -1,15 +1,91 @@
|
||||
<!-- Admin initialization page -->
|
||||
{% extends "base.html" %} {% block head %}
|
||||
<title>Initialize user</title>
|
||||
<title>Initialize admin user</title>
|
||||
<script>
|
||||
const checkEqualPasswords = function () {
|
||||
const first = document.getElementById("password").value;
|
||||
const second = document.getElementById("confirm_password").value;
|
||||
if (first !== second && first.length > 0 && second.length > 0) {
|
||||
document.getElementById("message").style.display = "block";
|
||||
document.getElementById("submit").disabled = true;
|
||||
} else {
|
||||
document.getElementById("message").style.display = "none";
|
||||
if (first.length > 0 && second.length > 0) {
|
||||
document.getElementById("submit").disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
const rule = /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d).{8,}$/;
|
||||
if (!rule.test(first)) {
|
||||
document.getElementById("password-req").style.display = "block";
|
||||
document.getElementById("submit").disabled = true;
|
||||
} else {
|
||||
document.getElementById("password-req").style.display = "none";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endblock %} {% block body %}
|
||||
<div class="h-screen w-full flex items-center justify-center">
|
||||
<form class="flex flex-col gap-1">
|
||||
<form class="flex flex-col gap-2 max-w-[30rem]" method="post" id="form">
|
||||
<script>
|
||||
// Redirect to the home page after initialization
|
||||
const onSubmit = event => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.target);
|
||||
console.log(formData);
|
||||
fetch("/init", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
alert("An error occurred. Please try again.");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
};
|
||||
document.getElementById("form").addEventListener("submit", onSubmit);
|
||||
</script>
|
||||
|
||||
<p>
|
||||
No user was found in the database. Please create an admin user to get set
|
||||
up.
|
||||
</p>
|
||||
<label for="username">Username</label>
|
||||
<input id="username" type="text" class="input" />
|
||||
<input id="username" name="username" type="text" class="input" required />
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input id="password" type="password" />
|
||||
<label for="password">Confirm password</label>
|
||||
<input id="password" type="password" />
|
||||
<button>Create account</button>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="input"
|
||||
onkeyup="checkEqualPasswords();"
|
||||
required
|
||||
/>
|
||||
|
||||
<label for="confirm_password">Confirm password</label>
|
||||
<input
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
type="password"
|
||||
class="input"
|
||||
onkeyup="checkEqualPasswords();"
|
||||
required
|
||||
/>
|
||||
|
||||
<span id="message" class="text-red-400 hidden">Passwords do not match</span>
|
||||
<span id="password-req" class="text-red-400 hidden"
|
||||
>Password must be at least 8 characters long and contain at least one
|
||||
uppercase letter, one lowercase letter, and one number</span
|
||||
>
|
||||
<button id="submit" class="btn btn-primary" type="submit" disabled>
|
||||
Create account
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user