Added a theme selector
This commit is contained in:
@@ -19,6 +19,7 @@ from backend.app.services.email_service import (
|
||||
smtp_configured,
|
||||
)
|
||||
from backend.app.services.email_verification import create_verification_token
|
||||
from backend.app.services.theme_service import THEMES, get_enabled_themes, save_enabled_themes
|
||||
from backend.app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
@@ -49,6 +50,10 @@ class AdminSmtpUpdate(BaseModel):
|
||||
smtp_use_tls: bool = True
|
||||
|
||||
|
||||
class AdminThemesUpdate(BaseModel):
|
||||
themes: list[str]
|
||||
|
||||
|
||||
def validate_smtp_values(payload: AdminSmtpUpdate, current: dict | None = None) -> dict:
|
||||
smtp_host = payload.smtp_host.strip()
|
||||
smtp_from = payload.smtp_from.strip()
|
||||
@@ -139,6 +144,20 @@ def get_admin_smtp_settings(_: dict = Depends(require_admin)):
|
||||
return public_smtp_settings(get_smtp_settings())
|
||||
|
||||
|
||||
@router.get('/themes')
|
||||
def get_admin_themes(_: dict = Depends(require_admin)):
|
||||
return {'themes': THEMES, 'enabled': get_enabled_themes()}
|
||||
|
||||
|
||||
@router.put('/themes')
|
||||
def update_admin_themes(payload: AdminThemesUpdate, _: dict = Depends(require_admin)):
|
||||
try:
|
||||
enabled = save_enabled_themes(payload.themes)
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=422, detail=str(error)) from error
|
||||
return {'themes': THEMES, 'enabled': enabled}
|
||||
|
||||
|
||||
@router.put('/smtp')
|
||||
def update_admin_smtp_settings(payload: AdminSmtpUpdate, _: dict = Depends(require_admin)):
|
||||
current = get_smtp_settings()
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from backend.app.services.link_service import list_public_links, list_public_users
|
||||
from backend.app.services.token_service import validate_token
|
||||
from backend.app.services.theme_service import THEMES, get_enabled_themes
|
||||
|
||||
router = APIRouter()
|
||||
optional_bearer = HTTPBearer(auto_error=False)
|
||||
@@ -16,6 +17,12 @@ def public_users():
|
||||
return list_public_users()
|
||||
|
||||
|
||||
@router.get('/themes')
|
||||
def public_themes():
|
||||
enabled = get_enabled_themes()
|
||||
return [{'id': theme, **THEMES[theme]} for theme in enabled]
|
||||
|
||||
|
||||
@router.get('/feed')
|
||||
@router.get('/feed/{username}')
|
||||
def public_feed(
|
||||
|
||||
@@ -35,14 +35,14 @@ def save_smtp_settings(values: dict) -> None:
|
||||
conn.commit()
|
||||
|
||||
|
||||
def smtp_configured() -> bool:
|
||||
smtp = get_smtp_settings()
|
||||
def smtp_configured(smtp_values: dict | None = None) -> bool:
|
||||
smtp = smtp_values or get_smtp_settings()
|
||||
return bool(smtp['smtp_host'] and smtp['smtp_from'])
|
||||
|
||||
|
||||
def send_message(email: str, subject: str, body: str, smtp_values: dict | None = None) -> None:
|
||||
smtp = smtp_values or get_smtp_settings()
|
||||
if not smtp_configured():
|
||||
if not smtp_configured(smtp):
|
||||
raise RuntimeError('SMTP is not configured; set LINKLOG_SMTP_HOST and LINKLOG_SMTP_FROM')
|
||||
|
||||
message = EmailMessage()
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
## Copyright © 2026 Olaf Kolkman
|
||||
## SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
|
||||
from backend.app.database import get_connection
|
||||
|
||||
|
||||
THEMES = {
|
||||
'plain-day': {'label': 'Plain Day', 'description': 'A bright, neutral daytime theme.'},
|
||||
'plain-night': {'label': 'Plain Night', 'description': 'A neutral dark nighttime theme.'},
|
||||
'latte': {'label': 'Catppuccin Latte', 'description': 'Catppuccin light theme.'},
|
||||
'frappe': {'label': 'Catppuccin Frappe', 'description': 'Catppuccin soft dark theme.'},
|
||||
'macchiato': {'label': 'Catppuccin Macchiato', 'description': 'Catppuccin medium dark theme.'},
|
||||
'mocha': {'label': 'Catppuccin Mocha', 'description': 'Catppuccin deep dark theme.'},
|
||||
'dracula': {'label': 'Dracula', 'description': 'A vivid dark theme with high-contrast accents.'},
|
||||
'nord': {'label': 'Nord', 'description': 'A cool, muted blue-gray theme.'},
|
||||
'solarized': {'label': 'Solarized', 'description': 'A balanced theme available in a light style.'},
|
||||
}
|
||||
DEFAULT_ENABLED_THEMES = tuple(THEMES)
|
||||
|
||||
|
||||
def get_enabled_themes() -> list[str]:
|
||||
with get_connection() as conn:
|
||||
row = conn.execute('SELECT value FROM app_settings WHERE name = ?', ('enabled_themes',)).fetchone()
|
||||
if row is None:
|
||||
return list(DEFAULT_ENABLED_THEMES)
|
||||
try:
|
||||
configured = json.loads(row['value'])
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return list(DEFAULT_ENABLED_THEMES)
|
||||
return [theme for theme in configured if theme in THEMES] or ['mocha']
|
||||
|
||||
|
||||
def save_enabled_themes(themes: list[str]) -> list[str]:
|
||||
selected = list(dict.fromkeys(theme for theme in themes if theme in THEMES))
|
||||
if not selected:
|
||||
raise ValueError('At least one theme must be enabled')
|
||||
with get_connection() as conn:
|
||||
conn.execute(
|
||||
'''INSERT INTO app_settings (name, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(name) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP''',
|
||||
('enabled_themes', json.dumps(selected)),
|
||||
)
|
||||
conn.commit()
|
||||
return selected
|
||||
@@ -62,9 +62,25 @@ def test_configuration_requires_authentication_and_admin_role():
|
||||
assert client.get('/api/admin/smtp', headers=login_headers('bob')).status_code == 403
|
||||
|
||||
|
||||
def test_admin_can_select_multiple_themes():
|
||||
headers = login_headers()
|
||||
response = client.put('/api/admin/themes', headers=headers, json={
|
||||
'themes': ['plain-day', 'plain-night', 'latte', 'frappe', 'macchiato', 'mocha', 'dracula', 'nord', 'solarized'],
|
||||
})
|
||||
assert response.status_code == 200
|
||||
assert response.json()['enabled'] == ['plain-day', 'plain-night', 'latte', 'frappe', 'macchiato', 'mocha', 'dracula', 'nord', 'solarized']
|
||||
public_response = client.get('/api/public/themes')
|
||||
assert public_response.status_code == 200
|
||||
assert [theme['id'] for theme in public_response.json()] == response.json()['enabled']
|
||||
|
||||
assert client.put('/api/admin/themes', headers=headers, json={'themes': []}).status_code == 422
|
||||
|
||||
|
||||
def test_admin_can_save_and_validate_smtp_settings():
|
||||
headers = login_headers()
|
||||
original = get_smtp_settings()
|
||||
with get_connection() as conn:
|
||||
original_row = conn.execute('SELECT value FROM app_settings WHERE name = ?', ('smtp',)).fetchone()
|
||||
response = client.put('/api/admin/smtp', headers=headers, json={
|
||||
'smtp_host': 'smtp.example.com',
|
||||
'smtp_port': 587,
|
||||
@@ -97,7 +113,18 @@ def test_admin_can_save_and_validate_smtp_settings():
|
||||
'smtp_use_tls': False,
|
||||
})
|
||||
|
||||
client.put('/api/admin/smtp', headers=headers, json=original)
|
||||
with get_connection() as conn:
|
||||
if original_row is None:
|
||||
conn.execute('DELETE FROM app_settings WHERE name = ?', ('smtp',))
|
||||
else:
|
||||
conn.execute(
|
||||
'UPDATE app_settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?',
|
||||
(original_row['value'], 'smtp'),
|
||||
)
|
||||
conn.commit()
|
||||
with get_connection() as conn:
|
||||
conn.execute('DELETE FROM app_settings WHERE name = ?', ('admin_smtp_mail_rate',))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_admin_reports_smtp_validation_errors():
|
||||
|
||||
Reference in New Issue
Block a user