Initial LinkLog implementation
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import json
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.app.api.dependencies import require_admin
|
||||
from backend.app.database import get_connection, hash_password
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class AdminPluginUpdate(BaseModel):
|
||||
enabled: bool | None = None
|
||||
config: dict | None = None
|
||||
|
||||
|
||||
class AdminUserCreate(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
password: str
|
||||
is_admin: bool = False
|
||||
|
||||
|
||||
class AdminUserUpdate(BaseModel):
|
||||
is_admin: bool
|
||||
|
||||
|
||||
def public_user(row):
|
||||
return {
|
||||
'id': row['id'],
|
||||
'username': row['username'],
|
||||
'email': row['email'],
|
||||
'is_admin': bool(row['is_admin']),
|
||||
'avatar_url': row['avatar_url'],
|
||||
'bio': row['bio'],
|
||||
'created_at': row['created_at'],
|
||||
}
|
||||
|
||||
|
||||
@router.get('/users')
|
||||
def list_users(_: dict = Depends(require_admin)):
|
||||
with get_connection() as conn:
|
||||
rows = conn.execute(
|
||||
'SELECT id, username, email, is_admin, avatar_url, bio, created_at FROM users ORDER BY username'
|
||||
).fetchall()
|
||||
return [public_user(row) for row in rows]
|
||||
|
||||
|
||||
@router.post('/users', status_code=201)
|
||||
def create_user(payload: AdminUserCreate, _: dict = Depends(require_admin)):
|
||||
username = payload.username.strip()
|
||||
email = payload.email.strip()
|
||||
if not username or not email or len(payload.password) < 8:
|
||||
raise HTTPException(status_code=422, detail='Username, email, and a password of at least 8 characters are required')
|
||||
|
||||
with get_connection() as conn:
|
||||
try:
|
||||
cursor = conn.execute(
|
||||
'''
|
||||
INSERT INTO users (id, username, email, password_hash, is_admin)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''',
|
||||
(str(uuid4()), username, email, hash_password(payload.password), int(payload.is_admin)),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as error:
|
||||
if 'UNIQUE constraint failed' in str(error):
|
||||
raise HTTPException(status_code=409, detail='Username or email already exists') from error
|
||||
raise
|
||||
|
||||
row = conn.execute(
|
||||
'SELECT id, username, email, is_admin, avatar_url, bio, created_at FROM users WHERE rowid = last_insert_rowid()'
|
||||
).fetchone()
|
||||
return public_user(row)
|
||||
|
||||
|
||||
@router.put('/users/{user_id}')
|
||||
def update_user_privileges(
|
||||
user_id: str,
|
||||
payload: AdminUserUpdate,
|
||||
_: dict = Depends(require_admin),
|
||||
):
|
||||
with get_connection() as conn:
|
||||
target = conn.execute(
|
||||
'SELECT id, is_admin FROM users WHERE id = ?',
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
if target is None:
|
||||
raise HTTPException(status_code=404, detail='User not found')
|
||||
|
||||
if target['is_admin'] and not payload.is_admin:
|
||||
admin_count = conn.execute(
|
||||
'SELECT COUNT(*) AS count FROM users WHERE is_admin = 1'
|
||||
).fetchone()['count']
|
||||
if admin_count <= 1:
|
||||
raise HTTPException(status_code=400, detail='At least one administrator is required')
|
||||
|
||||
conn.execute(
|
||||
'UPDATE users SET is_admin = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
(int(payload.is_admin), user_id),
|
||||
)
|
||||
conn.commit()
|
||||
row = conn.execute(
|
||||
'SELECT id, username, email, is_admin, avatar_url, bio, created_at FROM users WHERE id = ?',
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
return public_user(row)
|
||||
|
||||
|
||||
@router.delete('/users/{user_id}')
|
||||
def delete_user(user_id: str, current_user: dict = Depends(require_admin)):
|
||||
if user_id == current_user['id']:
|
||||
raise HTTPException(status_code=400, detail='You cannot delete your own account')
|
||||
|
||||
with get_connection() as conn:
|
||||
target = conn.execute('SELECT id, is_admin FROM users WHERE id = ?', (user_id,)).fetchone()
|
||||
if target is None:
|
||||
raise HTTPException(status_code=404, detail='User not found')
|
||||
if target['is_admin']:
|
||||
admins = conn.execute('SELECT COUNT(*) AS count FROM users WHERE is_admin = 1').fetchone()['count']
|
||||
if admins <= 1:
|
||||
raise HTTPException(status_code=400, detail='Cannot delete the last administrator')
|
||||
|
||||
conn.execute('DELETE FROM users WHERE id = ?', (user_id,))
|
||||
conn.commit()
|
||||
return {'status': 'deleted', 'id': user_id}
|
||||
|
||||
|
||||
@router.get('/plugins')
|
||||
def list_plugins(_: dict = Depends(require_admin)):
|
||||
with get_connection() as conn:
|
||||
rows = conn.execute(
|
||||
'SELECT id, name, version, enabled, config FROM plugins ORDER BY name'
|
||||
).fetchall()
|
||||
return [
|
||||
{
|
||||
'id': row['id'],
|
||||
'name': row['name'],
|
||||
'version': row['version'],
|
||||
'enabled': bool(row['enabled']),
|
||||
'config': row['config'],
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
@router.get('/plugins/{plugin_name}')
|
||||
def get_plugin(plugin_name: str, _: dict = Depends(require_admin)):
|
||||
with get_connection() as conn:
|
||||
row = conn.execute(
|
||||
'SELECT id, name, version, enabled, config FROM plugins WHERE name = ?',
|
||||
(plugin_name,),
|
||||
).fetchone()
|
||||
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail='Plugin not found')
|
||||
|
||||
return {
|
||||
'id': row['id'],
|
||||
'name': row['name'],
|
||||
'version': row['version'],
|
||||
'enabled': bool(row['enabled']),
|
||||
'config': json.loads(row['config']) if row['config'] else {},
|
||||
}
|
||||
|
||||
|
||||
@router.put('/plugins/{plugin_name}')
|
||||
def update_plugin(
|
||||
plugin_name: str,
|
||||
payload: AdminPluginUpdate,
|
||||
_: dict = Depends(require_admin),
|
||||
):
|
||||
with get_connection() as conn:
|
||||
current = conn.execute(
|
||||
'SELECT id, name, version, enabled, config FROM plugins WHERE name = ?',
|
||||
(plugin_name,),
|
||||
).fetchone()
|
||||
|
||||
if current is None:
|
||||
raise HTTPException(status_code=404, detail='Plugin not found')
|
||||
|
||||
enabled = payload.enabled if payload.enabled is not None else bool(current['enabled'])
|
||||
config = json.loads(current['config']) if current['config'] else {}
|
||||
if payload.config is not None:
|
||||
config.update(payload.config)
|
||||
|
||||
conn.execute(
|
||||
'''
|
||||
UPDATE plugins
|
||||
SET enabled = ?, config = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE name = ?
|
||||
''',
|
||||
(1 if enabled else 0, json.dumps(config, ensure_ascii=False), plugin_name),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
return {
|
||||
'name': plugin_name,
|
||||
'enabled': enabled,
|
||||
'config': config,
|
||||
}
|
||||
Reference in New Issue
Block a user