Complete authentication and profile features

This commit is contained in:
Olaf
2026-08-24 14:50:05 +02:00
parent 1c827956c4
commit 01702fcadf
17 changed files with 227 additions and 40 deletions
+6 -1
View File
@@ -1,10 +1,15 @@
from fastapi import APIRouter
from backend.app.services.link_service import list_public_links
from backend.app.services.link_service import list_public_links, list_public_users
router = APIRouter()
@router.get('/users')
def public_users():
return list_public_users()
@router.get('/feed')
@router.get('/feed/{username}')
def public_feed(username: str | None = None):
+39 -6
View File
@@ -1,11 +1,11 @@
import json
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from pydantic import BaseModel
from backend.app.api.dependencies import get_current_user
from backend.app.database import get_connection
from backend.app.database import AVATARS_DIR, get_connection
router = APIRouter()
@@ -13,7 +13,6 @@ router = APIRouter()
class UserConfigUpdate(BaseModel):
email: str | None = None
bio: str | None = None
avatar_url: str | None = None
class UserPluginConfigUpdate(BaseModel):
@@ -47,21 +46,55 @@ def update_current_user_profile(
email = payload.email or current['email']
bio = payload.bio if payload.bio is not None else current['bio']
avatar_url = payload.avatar_url if payload.avatar_url is not None else current['avatar_url']
conn.execute(
'''
UPDATE users
SET email = ?, bio = ?, avatar_url = ?, updated_at = CURRENT_TIMESTAMP
SET email = ?, bio = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''',
(email, bio, avatar_url, user['id']),
(email, bio, user['id']),
)
conn.commit()
return {'status': 'updated'}
@router.post('/avatar')
async def upload_avatar(
avatar: UploadFile = File(...),
user: dict = Depends(get_current_user),
):
allowed_types = {
'image/gif': '.gif',
'image/jpeg': '.jpg',
'image/png': '.png',
'image/webp': '.webp',
}
suffix = allowed_types.get(avatar.content_type or '')
if suffix is None:
raise HTTPException(status_code=415, detail='Avatar must be a PNG, JPEG, GIF, or WebP image')
contents = await avatar.read(2 * 1024 * 1024 + 1)
if len(contents) > 2 * 1024 * 1024:
raise HTTPException(status_code=413, detail='Avatar must be 2 MB or smaller')
avatar_path = AVATARS_DIR / f'{user["id"]}{suffix}'
for existing_path in AVATARS_DIR.glob(f'{user["id"]}.*'):
if existing_path != avatar_path:
existing_path.unlink(missing_ok=True)
avatar_path.write_bytes(contents)
avatar_url = f'/media/{avatar_path.name}'
with get_connection() as conn:
conn.execute(
'UPDATE users SET avatar_url = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
(avatar_url, user['id']),
)
conn.commit()
return {'avatar_url': avatar_url}
@router.get('/plugins/{plugin_name}')
def get_user_plugin_config(plugin_name: str, user: dict = Depends(get_current_user)):
with get_connection() as conn:
+2
View File
@@ -6,6 +6,8 @@ from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
DB_PATH = Path(os.getenv('LINKLOG_DATABASE_PATH', BASE_DIR / 'data' / 'linklog.db'))
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
AVATARS_DIR = DB_PATH.parent / 'avatars'
AVATARS_DIR.mkdir(parents=True, exist_ok=True)
def hash_password(password: str) -> str:
+3
View File
@@ -9,10 +9,12 @@ from backend.app.api.auth import router as auth_router
from backend.app.api.links import router as links_router
from backend.app.api.public import router as public_router
from backend.app.api.user_config import router as user_config_router
from backend.app.database import AVATARS_DIR
from backend.app.services.link_service import list_public_links
app = FastAPI(title='LinkLog API')
app.mount('/static', StaticFiles(directory='frontend/static'), name='static')
app.mount('/media', StaticFiles(directory=AVATARS_DIR), name='media')
app.include_router(auth_router, prefix='/api/auth')
app.include_router(links_router, prefix='/api')
app.include_router(public_router, prefix='/api/public')
@@ -49,6 +51,7 @@ def health_check():
@app.get('/{username}', response_class=HTMLResponse)
@app.get('/{username}/', response_class=HTMLResponse)
async def public_user_feed(request: Request, username: str):
feed = list_public_links(username)
profile = None
+8
View File
@@ -55,3 +55,11 @@ def list_public_links(username: str | None = None):
(username, username),
).fetchall()
return [dict(row) for row in rows]
def list_public_users():
with get_connection() as conn:
rows = conn.execute(
'SELECT username FROM users ORDER BY username'
).fetchall()
return [row['username'] for row in rows]