Remove mastodon posts on delete and OTP

This commit is contained in:
2026-08-26 14:16:29 +02:00
parent 80a3a3d541
commit f503dbaef2
24 changed files with 388 additions and 11 deletions
+5 -1
View File
@@ -13,6 +13,7 @@ from backend.app.services.email_service import send_password_reset_email, smtp_c
from backend.app.services.email_verification import verify_email
from backend.app.services.password_reset import create_reset_token, reset_password
from backend.app.services.token_service import issue_token, revoke_token, validate_token
from backend.app.services.otp_service import verify_code
router = APIRouter()
@@ -22,6 +23,7 @@ init_db()
class LoginRequest(BaseModel):
username: str
password: str
otp: str | None = None
class PasswordResetRequest(BaseModel):
@@ -44,6 +46,8 @@ def login(payload: LoginRequest):
raise HTTPException(status_code=401, detail='Invalid username or password')
if not user['email_verified']:
raise HTTPException(status_code=403, detail='Email address is not verified')
if user['otp_enabled'] and not verify_code(user['otp_secret'], payload.otp):
raise HTTPException(status_code=401, detail='One-time password required or invalid')
token_data = issue_token(user['id'], user['username'])
return {
@@ -51,7 +55,7 @@ def login(payload: LoginRequest):
'token_type': 'bearer',
'expires_at': token_data['expires_at'],
'refresh_token': token_data['refresh_token'],
'user': {'id': user['id'], 'username': user['username'], 'email': user['email']}
'user': {'id': user['id'], 'username': user['username'], 'email': user['email'], 'otp_enabled': bool(user['otp_enabled'])}
}
+12 -1
View File
@@ -1,11 +1,12 @@
## Copyright © 2026 Olaf Kolkman
## SPDX-License-Identifier: GPL-3.0-or-later
import json
from fastapi import APIRouter, Header, HTTPException, status
import logging
from pydantic import BaseModel
from backend.app.services.link_service import create_link, delete_link, get_link_tags, list_public_links, list_tags, mark_mastodon_posted, update_link
from backend.app.services.link_service import create_link, delete_link, get_link_tags, get_owned_link, list_public_links, list_tags, mark_mastodon_posted, update_link
from backend.app.database import get_connection
from backend.app.services.plugin_manager import plugin_manager
from backend.app.services.token_service import validate_token
@@ -87,6 +88,16 @@ def delete_link_endpoint(
info = validate_token(authorization.replace('Bearer ', '', 1))
if info is None:
raise HTTPException(status_code=401, detail='Token expired or invalid')
link = get_owned_link(link_id, info['user_id'])
if link is None:
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
post_ids = json.loads(link['mastodon_post_ids']) if link.get('mastodon_post_ids') else []
if not post_ids and link.get('mastodon_post_id'):
post_ids = [link['mastodon_post_id']]
if post_ids:
result = plugin_manager.delete_mastodon_posts({**link, 'mastodon_post_ids': post_ids})
if result.get('status') != 'deleted':
raise HTTPException(status_code=502, detail=result.get('reason', 'Could not delete Mastodon posts'))
if not delete_link(link_id, info['user_id']):
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
return {'status': 'deleted', 'id': link_id}
+41 -1
View File
@@ -10,6 +10,7 @@ from pydantic import BaseModel
from backend.app.api.dependencies import get_current_user
from backend.app.database import AVATARS_DIR, get_connection, hash_password
from backend.app.services.link_service import create_label, delete_label, list_user_labels, update_label
from backend.app.services.otp_service import create_secret, provisioning_uri, verify_code
router = APIRouter()
@@ -24,6 +25,11 @@ class PasswordUpdate(BaseModel):
new_password: str
class OtpUpdate(BaseModel):
action: str
code: str | None = None
class UserPluginConfigUpdate(BaseModel):
instance: str | None = None
access_token: str | None = None
@@ -44,7 +50,10 @@ def get_current_user_profile(user: dict = Depends(get_current_user)):
).fetchone()
if row is None:
raise HTTPException(status_code=404, detail='User not found')
return dict(row)
profile = dict(row)
profile.pop('otp_secret', None)
profile.pop('password_hash', None)
return profile
@router.put('/me')
@@ -89,6 +98,37 @@ def update_password(payload: PasswordUpdate, user: dict = Depends(get_current_us
return {'status': 'password_updated'}
@router.get('/otp')
def get_otp(user: dict = Depends(get_current_user)):
return {'enabled': bool(user['otp_enabled'])}
@router.post('/otp/setup')
def setup_otp(user: dict = Depends(get_current_user)):
if user['otp_enabled']:
raise HTTPException(status_code=409, detail='One-time password is already enabled')
secret = create_secret()
with get_connection() as conn:
conn.execute('UPDATE users SET otp_secret = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', (secret, user['id']))
conn.commit()
return {'secret': secret, 'otpauth_url': provisioning_uri(secret, user['username'])}
@router.post('/otp')
def update_otp(payload: OtpUpdate, user: dict = Depends(get_current_user)):
if payload.action not in {'enable', 'disable'}:
raise HTTPException(status_code=422, detail='OTP action must be enable or disable')
if not verify_code(user['otp_secret'], payload.code):
raise HTTPException(status_code=400, detail='Invalid one-time password')
with get_connection() as conn:
if payload.action == 'enable':
conn.execute('UPDATE users SET otp_enabled = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?', (user['id'],))
else:
conn.execute('UPDATE users SET otp_enabled = 0, otp_secret = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?', (user['id'],))
conn.commit()
return {'status': 'updated', 'enabled': payload.action == 'enable'}
@router.get('/labels')
def get_labels(user: dict = Depends(get_current_user)):
return list_user_labels(user['id'])