ruff and prettier fixes
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m6s
Python Code Quality / python-code-quality (pull_request) Successful in 10s
Python Test / python-test (pull_request) Successful in 15s

This commit is contained in:
Brian Bjarke Jensen
2025-11-07 23:10:59 +01:00
parent 469fd722de
commit 2b956213aa
10 changed files with 644 additions and 655 deletions
+1
View File
@@ -6,6 +6,7 @@ from datetime import datetime
if TYPE_CHECKING: if TYPE_CHECKING:
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
Base = DeclarativeBase Base = DeclarativeBase
else: else:
from baby_monitor.repositories.dependencies.get_database import Base from baby_monitor.repositories.dependencies.get_database import Base
+3 -6
View File
@@ -11,6 +11,7 @@ from sqlalchemy.orm import Mapped, mapped_column
if TYPE_CHECKING: if TYPE_CHECKING:
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
Base = DeclarativeBase Base = DeclarativeBase
else: else:
from baby_monitor.repositories.dependencies.get_database import Base from baby_monitor.repositories.dependencies.get_database import Base
@@ -21,9 +22,7 @@ class Invitation(Base):
__tablename__ = "invitations" __tablename__ = "invitations"
id: Mapped[int] = mapped_column( id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
Integer, primary_key=True, autoincrement=True
)
token: Mapped[str] = mapped_column(String, unique=True, nullable=False) token: Mapped[str] = mapped_column(String, unique=True, nullable=False)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False DateTime(timezone=True), nullable=False
@@ -32,9 +31,7 @@ class Invitation(Base):
DateTime(timezone=True), nullable=False DateTime(timezone=True), nullable=False
) )
created_by_user_id: Mapped[int] = mapped_column(Integer, nullable=False) created_by_user_id: Mapped[int] = mapped_column(Integer, nullable=False)
is_consumed: Mapped[bool] = mapped_column( is_consumed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
Boolean, default=False, nullable=False
)
consumed_at: Mapped[datetime | None] = mapped_column( consumed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True DateTime(timezone=True), nullable=True
) )
@@ -34,9 +34,7 @@ class SQLiteInvitationRepository(InvitationRepositoryInterface):
# Convert timezone-aware datetimes to naive UTC for SQLite # Convert timezone-aware datetimes to naive UTC for SQLite
created_at_utc = datetime.now(UTC).replace(tzinfo=None) created_at_utc = datetime.now(UTC).replace(tzinfo=None)
expires_at_utc = ( expires_at_utc = (
expires_at.replace(tzinfo=None) expires_at.replace(tzinfo=None) if expires_at.tzinfo else expires_at
if expires_at.tzinfo
else expires_at
) )
invitation = Invitation( invitation = Invitation(
@@ -60,9 +58,7 @@ class SQLiteInvitationRepository(InvitationRepositoryInterface):
Returns: Returns:
True if valid and not consumed, False otherwise True if valid and not consumed, False otherwise
""" """
invitation = ( invitation = self.db.query(Invitation).filter(Invitation.token == token).first()
self.db.query(Invitation).filter(Invitation.token == token).first()
)
if not invitation: if not invitation:
return False return False
@@ -88,9 +84,7 @@ class SQLiteInvitationRepository(InvitationRepositoryInterface):
Returns: Returns:
True if successfully consumed, False if invalid or already used True if successfully consumed, False if invalid or already used
""" """
invitation = ( invitation = self.db.query(Invitation).filter(Invitation.token == token).first()
self.db.query(Invitation).filter(Invitation.token == token).first()
)
if not invitation: if not invitation:
return False return False
@@ -114,7 +108,5 @@ class SQLiteInvitationRepository(InvitationRepositoryInterface):
def cleanup_expired(self) -> None: def cleanup_expired(self) -> None:
"""Remove expired invitation tokens from storage.""" """Remove expired invitation tokens from storage."""
now_utc = datetime.now(UTC).replace(tzinfo=None) now_utc = datetime.now(UTC).replace(tzinfo=None)
self.db.query(Invitation).filter( self.db.query(Invitation).filter(Invitation.expires_at < now_utc).delete()
Invitation.expires_at < now_utc
).delete()
self.db.commit() self.db.commit()
+9 -27
View File
@@ -52,9 +52,7 @@ def verify_token(
def verify_admin( def verify_admin(
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
user_repo: Annotated[ user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)],
UserRepositoryInterface, Depends(get_user_repository)
],
) -> int: ) -> int:
"""Verify the user is an admin and return user_id.""" """Verify the user is an admin and return user_id."""
# First try to get user from database # First try to get user from database
@@ -63,9 +61,7 @@ def verify_admin(
if user: if user:
# Database user - check is_admin field # Database user - check is_admin field
if not user.get("is_admin", False): if not user.get("is_admin", False):
raise HTTPException( raise HTTPException(status_code=403, detail="Admin access required")
status_code=403, detail="Admin access required"
)
return user_id return user_id
# If not in database but has valid token with user_id=1, # If not in database but has valid token with user_id=1,
@@ -80,12 +76,8 @@ def verify_admin(
@router.post("/login", response_model=LoginResponse) @router.post("/login", response_model=LoginResponse)
def login( def login(
credentials: LoginRequest, credentials: LoginRequest,
token_repo: Annotated[ token_repo: Annotated[TokenRepositoryInterface, Depends(get_token_repository)],
TokenRepositoryInterface, Depends(get_token_repository) user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)],
],
user_repo: Annotated[
UserRepositoryInterface, Depends(get_user_repository)
],
creds_repo: Annotated[ creds_repo: Annotated[
CredentialsRepositoryInterface, Depends(get_credentials_repository) CredentialsRepositoryInterface, Depends(get_credentials_repository)
], ],
@@ -114,9 +106,7 @@ def login(
) )
# Fallback to admin credentials from environment # Fallback to admin credentials from environment
if creds_repo.verify_admin_credentials( if creds_repo.verify_admin_credentials(credentials.username, credentials.password):
credentials.username, credentials.password
):
# Generate a secure random token # Generate a secure random token
access_token = secrets.token_urlsafe(32) access_token = secrets.token_urlsafe(32)
# Store token with user_id (hardcoded 1 for admin) # Store token with user_id (hardcoded 1 for admin)
@@ -129,9 +119,7 @@ def login(
is_admin=True, is_admin=True,
) )
raise HTTPException( raise HTTPException(status_code=401, detail="Invalid username or password")
status_code=401, detail="Invalid username or password"
)
@router.post("/logout") @router.post("/logout")
@@ -162,15 +150,11 @@ def verify_invitation(
@router.post("/register", response_model=RegisterResponse) @router.post("/register", response_model=RegisterResponse)
def register( def register(
request: RegisterRequest, request: RegisterRequest,
user_repo: Annotated[ user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)],
UserRepositoryInterface, Depends(get_user_repository)
],
invitation_repo: Annotated[ invitation_repo: Annotated[
InvitationRepositoryInterface, Depends(get_invitation_repository) InvitationRepositoryInterface, Depends(get_invitation_repository)
], ],
token_repo: Annotated[ token_repo: Annotated[TokenRepositoryInterface, Depends(get_token_repository)],
TokenRepositoryInterface, Depends(get_token_repository)
],
) -> RegisterResponse: ) -> RegisterResponse:
""" """
Register a new user with an invitation token. Register a new user with an invitation token.
@@ -218,9 +202,7 @@ def register(
@router.get("/me") @router.get("/me")
def get_current_user( def get_current_user(
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
user_repo: Annotated[ user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)],
UserRepositoryInterface, Depends(get_user_repository)
],
creds_repo: Annotated[ creds_repo: Annotated[
CredentialsRepositoryInterface, Depends(get_credentials_repository) CredentialsRepositoryInterface, Depends(get_credentials_repository)
], ],
+60 -56
View File
@@ -1,8 +1,8 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor - Admin</title> <title>Baby Monitor - Admin</title>
<style> <style>
* { * {
@@ -12,7 +12,9 @@
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
@@ -55,7 +57,7 @@
} }
.section h2::before { .section h2::before {
content: '🔗'; content: "🔗";
margin-right: 8px; margin-right: 8px;
} }
@@ -68,7 +70,9 @@
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s; transition:
transform 0.2s,
box-shadow 0.2s;
width: 100%; width: 100%;
} }
@@ -120,7 +124,7 @@
background: white; background: white;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
font-size: 13px; font-size: 13px;
word-break: break-all; word-break: break-all;
color: #333; color: #333;
@@ -148,7 +152,7 @@
.info-box { .info-box {
background: #e7f3ff; background: #e7f3ff;
border-left: 4px solid #2196F3; border-left: 4px solid #2196f3;
padding: 12px; padding: 12px;
border-radius: 4px; border-radius: 4px;
margin-top: 15px; margin-top: 15px;
@@ -182,15 +186,15 @@
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4); box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>👨‍💼 Admin Dashboard</h1> <h1>👨‍💼 Admin Dashboard</h1>
<p class="subtitle">Manage user invitations and settings</p> <p class="subtitle">Manage user invitations and settings</p>
<div class="section"> <div class="section">
<h2>Generate Invitation Link</h2> <h2>Generate Invitation Link</h2>
<p style="color: #666; margin-bottom: 15px; font-size: 14px;"> <p style="color: #666; margin-bottom: 15px; font-size: 14px">
Create a secure one-time link to invite a new user to register. Create a secure one-time link to invite a new user to register.
</p> </p>
<button class="button" id="generateBtn" onclick="generateLink()"> <button class="button" id="generateBtn" onclick="generateLink()">
@@ -206,62 +210,60 @@
</button> </button>
</div> </div>
<div class="info-box"> <div class="info-box">
️ This link expires in 24 hours and can only be used once. ️ This link expires in 24 hours and can only be used once. Share it
Share it securely with the intended user. securely with the intended user.
</div> </div>
</div> </div>
<div id="error" class="error"></div> <div id="error" class="error"></div>
</div> </div>
<button class="button logout-button" onclick="logout()"> <button class="button logout-button" onclick="logout()">Logout</button>
Logout
</button>
</div> </div>
<script> <script>
// Check if user is authenticated and is admin // Check if user is authenticated and is admin
const token = localStorage.getItem('access_token'); const token = localStorage.getItem("access_token");
if (!token) { if (!token) {
window.location.href = '/login.html'; window.location.href = "/login.html";
} }
// Verify user is admin // Verify user is admin
fetch('/api/me', { fetch("/api/me", {
headers: { headers: {
'Authorization': `Bearer ${token}`, Authorization: `Bearer ${token}`,
} },
}) })
.then(response => response.json()) .then((response) => response.json())
.then(user => { .then((user) => {
if (!user.is_admin) { if (!user.is_admin) {
alert('Access denied. Admin privileges required.'); alert("Access denied. Admin privileges required.");
window.location.href = '/'; window.location.href = "/";
} }
}) })
.catch(error => { .catch((error) => {
console.error('Error verifying admin status:', error); console.error("Error verifying admin status:", error);
window.location.href = '/login.html'; window.location.href = "/login.html";
}); });
async function generateLink() { async function generateLink() {
const btn = document.getElementById('generateBtn'); const btn = document.getElementById("generateBtn");
const linkContainer = document.getElementById('linkContainer'); const linkContainer = document.getElementById("linkContainer");
const linkText = document.getElementById('linkText'); const linkText = document.getElementById("linkText");
const errorDiv = document.getElementById('error'); const errorDiv = document.getElementById("error");
// Disable button and show loading state // Disable button and show loading state
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Generating...'; btn.textContent = "Generating...";
errorDiv.classList.remove('show'); errorDiv.classList.remove("show");
try { try {
const response = await fetch('/api/admin/generate-invitation', { const response = await fetch("/api/admin/generate-invitation", {
method: 'POST', method: "POST",
headers: { headers: {
'Authorization': `Bearer ${token}`, Authorization: `Bearer ${token}`,
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}); });
if (!response.ok) { if (!response.ok) {
@@ -276,40 +278,42 @@
// Display the link // Display the link
linkText.textContent = inviteUrl; linkText.textContent = inviteUrl;
linkContainer.classList.add('show'); linkContainer.classList.add("show");
} catch (error) { } catch (error) {
errorDiv.textContent = `Error: ${error.message}`; errorDiv.textContent = `Error: ${error.message}`;
errorDiv.classList.add('show'); errorDiv.classList.add("show");
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.textContent = 'Generate New Invitation Link'; btn.textContent = "Generate New Invitation Link";
} }
} }
function copyLink() { function copyLink() {
const linkText = document.getElementById('linkText').textContent; const linkText = document.getElementById("linkText").textContent;
const copyBtn = document.getElementById('copyBtn'); const copyBtn = document.getElementById("copyBtn");
navigator.clipboard.writeText(linkText).then(() => { navigator.clipboard
.writeText(linkText)
.then(() => {
// Show success state // Show success state
copyBtn.textContent = '✓ Copied!'; copyBtn.textContent = "✓ Copied!";
copyBtn.classList.add('copied'); copyBtn.classList.add("copied");
// Reset after 2 seconds // Reset after 2 seconds
setTimeout(() => { setTimeout(() => {
copyBtn.textContent = 'Copy'; copyBtn.textContent = "Copy";
copyBtn.classList.remove('copied'); copyBtn.classList.remove("copied");
}, 2000); }, 2000);
}).catch(err => { })
alert('Failed to copy link: ' + err); .catch((err) => {
alert("Failed to copy link: " + err);
}); });
} }
function logout() { function logout() {
localStorage.removeItem('access_token'); localStorage.removeItem("access_token");
window.location.href = '/login.html'; window.location.href = "/login.html";
} }
</script> </script>
</body> </body>
</html> </html>
+1 -3
View File
@@ -129,9 +129,7 @@
<div class="menu-backdrop" id="menuBackdrop"></div> <div class="menu-backdrop" id="menuBackdrop"></div>
<div class="menu-overlay" id="menuOverlay"> <div class="menu-overlay" id="menuOverlay">
<div class="menu-item logout" id="menuLogout"> <div class="menu-item logout" id="menuLogout">🚪 Logout</div>
🚪 Logout
</div>
</div> </div>
<div class="container"> <div class="container">
+67 -52
View File
@@ -1,8 +1,8 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor - Register</title> <title>Baby Monitor - Register</title>
<style> <style>
* { * {
@@ -12,7 +12,9 @@
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
@@ -80,7 +82,9 @@
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s; transition:
transform 0.2s,
box-shadow 0.2s;
margin-top: 10px; margin-top: 10px;
} }
@@ -145,13 +149,17 @@
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
.info-box { .info-box {
background: #e7f3ff; background: #e7f3ff;
border-left: 4px solid #2196F3; border-left: 4px solid #2196f3;
padding: 12px; padding: 12px;
border-radius: 4px; border-radius: 4px;
margin-bottom: 20px; margin-bottom: 20px;
@@ -173,12 +181,12 @@
margin: 5px 0 0 20px; margin: 5px 0 0 20px;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div id="loadingContainer" class="loading-container"> <div id="loadingContainer" class="loading-container">
<div class="spinner"></div> <div class="spinner"></div>
<p style="color: #666;">Validating invitation...</p> <p style="color: #666">Validating invitation...</p>
</div> </div>
<div id="registerContainer" class="hidden"> <div id="registerContainer" class="hidden">
@@ -199,7 +207,7 @@
minlength="3" minlength="3"
autocomplete="username" autocomplete="username"
placeholder="Choose a username" placeholder="Choose a username"
> />
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -212,7 +220,7 @@
minlength="8" minlength="8"
autocomplete="new-password" autocomplete="new-password"
placeholder="Choose a strong password" placeholder="Choose a strong password"
> />
<div class="password-requirements"> <div class="password-requirements">
<ul> <ul>
<li>At least 8 characters long</li> <li>At least 8 characters long</li>
@@ -231,7 +239,7 @@
minlength="8" minlength="8"
autocomplete="new-password" autocomplete="new-password"
placeholder="Re-enter your password" placeholder="Re-enter your password"
> />
</div> </div>
<button type="submit" class="button" id="submitBtn"> <button type="submit" class="button" id="submitBtn">
@@ -245,13 +253,13 @@
<p class="subtitle">This invitation link is invalid or has expired</p> <p class="subtitle">This invitation link is invalid or has expired</p>
<div class="info-box"> <div class="info-box">
This could mean: This could mean:
<ul style="margin: 10px 0 0 20px; color: #555;"> <ul style="margin: 10px 0 0 20px; color: #555">
<li>The invitation has already been used</li> <li>The invitation has already been used</li>
<li>The invitation has expired (24 hours)</li> <li>The invitation has expired (24 hours)</li>
<li>The invitation link is incorrect</li> <li>The invitation link is incorrect</li>
</ul> </ul>
</div> </div>
<p style="text-align: center; color: #666; margin-top: 20px;"> <p style="text-align: center; color: #666; margin-top: 20px">
Please contact an administrator for a new invitation link. Please contact an administrator for a new invitation link.
</p> </p>
</div> </div>
@@ -262,10 +270,10 @@
// Extract token from URL query parameters // Extract token from URL query parameters
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
invitationToken = urlParams.get('token'); invitationToken = urlParams.get("token");
// Validate token on page load // Validate token on page load
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener("DOMContentLoaded", async () => {
if (!invitationToken) { if (!invitationToken) {
showInvalidToken(); showInvalidToken();
return; return;
@@ -273,7 +281,9 @@
try { try {
// Verify the invitation token // Verify the invitation token
const response = await fetch(`/api/verify-invitation?token=${encodeURIComponent(invitationToken)}`); const response = await fetch(
`/api/verify-invitation?token=${encodeURIComponent(invitationToken)}`,
);
if (response.ok) { if (response.ok) {
showRegisterForm(); showRegisterForm();
@@ -281,51 +291,54 @@
showInvalidToken(); showInvalidToken();
} }
} catch (error) { } catch (error) {
console.error('Error validating invitation:', error); console.error("Error validating invitation:", error);
showInvalidToken(); showInvalidToken();
} }
}); });
function showRegisterForm() { function showRegisterForm() {
document.getElementById('loadingContainer').classList.add('hidden'); document.getElementById("loadingContainer").classList.add("hidden");
document.getElementById('registerContainer').classList.remove('hidden'); document.getElementById("registerContainer").classList.remove("hidden");
} }
function showInvalidToken() { function showInvalidToken() {
document.getElementById('loadingContainer').classList.add('hidden'); document.getElementById("loadingContainer").classList.add("hidden");
document.getElementById('invalidTokenContainer').classList.remove('hidden'); document
.getElementById("invalidTokenContainer")
.classList.remove("hidden");
} }
async function handleRegister(event) { async function handleRegister(event) {
event.preventDefault(); event.preventDefault();
const username = document.getElementById('username').value; const username = document.getElementById("username").value;
const password = document.getElementById('password').value; const password = document.getElementById("password").value;
const confirmPassword = document.getElementById('confirmPassword').value; const confirmPassword =
const errorDiv = document.getElementById('error'); document.getElementById("confirmPassword").value;
const successDiv = document.getElementById('success'); const errorDiv = document.getElementById("error");
const submitBtn = document.getElementById('submitBtn'); const successDiv = document.getElementById("success");
const submitBtn = document.getElementById("submitBtn");
// Clear previous messages // Clear previous messages
errorDiv.classList.remove('show'); errorDiv.classList.remove("show");
successDiv.classList.remove('show'); successDiv.classList.remove("show");
// Validate passwords match // Validate passwords match
if (password !== confirmPassword) { if (password !== confirmPassword) {
errorDiv.textContent = 'Passwords do not match!'; errorDiv.textContent = "Passwords do not match!";
errorDiv.classList.add('show'); errorDiv.classList.add("show");
return; return;
} }
// Disable submit button // Disable submit button
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.textContent = 'Creating Account...'; submitBtn.textContent = "Creating Account...";
try { try {
const response = await fetch('/api/register', { const response = await fetch("/api/register", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
username: username, username: username,
@@ -338,31 +351,33 @@
if (response.ok) { if (response.ok) {
// Store the access token and username // Store the access token and username
localStorage.setItem('access_token', data.access_token); localStorage.setItem("access_token", data.access_token);
localStorage.setItem('username', data.username); localStorage.setItem("username", data.username);
// Show success message // Show success message
successDiv.textContent = 'Account created successfully! Redirecting...'; successDiv.textContent =
successDiv.classList.add('show'); "Account created successfully! Redirecting...";
successDiv.classList.add("show");
// Redirect to home page after a brief delay // Redirect to home page after a brief delay
setTimeout(() => { setTimeout(() => {
window.location.href = '/'; window.location.href = "/";
}, 1500); }, 1500);
} else { } else {
errorDiv.textContent = data.detail || 'Registration failed. Please try again.'; errorDiv.textContent =
errorDiv.classList.add('show'); data.detail || "Registration failed. Please try again.";
errorDiv.classList.add("show");
submitBtn.disabled = false; submitBtn.disabled = false;
submitBtn.textContent = 'Create Account'; submitBtn.textContent = "Create Account";
} }
} catch (error) { } catch (error) {
console.error('Registration error:', error); console.error("Registration error:", error);
errorDiv.textContent = 'An error occurred. Please try again.'; errorDiv.textContent = "An error occurred. Please try again.";
errorDiv.classList.add('show'); errorDiv.classList.add("show");
submitBtn.disabled = false; submitBtn.disabled = false;
submitBtn.textContent = 'Create Account'; submitBtn.textContent = "Create Account";
} }
} }
</script> </script>
</body> </body>
</html> </html>