Add centralized authorization module and fix security vulnerabilities
- Create auth.py with AuthContext, require_super_user, require_authenticated - Fix 6 CRITICAL unprotected endpoints exposing sensitive data - Consolidate 16+ admin endpoints with duplicated super_user checks - Standardize on user_id (wallet.wallet.user) instead of wallet_id 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e403ec223d
commit
ca0cee7312
2 changed files with 412 additions and 191 deletions
310
auth.py
Normal file
310
auth.py
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
"""
|
||||||
|
Centralized Authorization Module for Castle Extension.
|
||||||
|
|
||||||
|
Provides consistent, secure authorization patterns across all endpoints.
|
||||||
|
|
||||||
|
Key concepts:
|
||||||
|
- AuthContext: Captures all authorization state for a request
|
||||||
|
- Dependencies: FastAPI dependencies for endpoint protection
|
||||||
|
- Permission checks: Consistent resource-level access control
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from .auth import require_super_user, require_authenticated, AuthContext
|
||||||
|
|
||||||
|
@router.get("/api/v1/admin-endpoint")
|
||||||
|
async def admin_endpoint(auth: AuthContext = Depends(require_super_user)):
|
||||||
|
# Only super users can access
|
||||||
|
pass
|
||||||
|
|
||||||
|
@router.get("/api/v1/user-data")
|
||||||
|
async def user_data(auth: AuthContext = Depends(require_authenticated)):
|
||||||
|
# Any authenticated user
|
||||||
|
user_id = auth.user_id
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import wraps
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
from lnbits.core.models import WalletTypeInfo
|
||||||
|
from lnbits.decorators import require_admin_key, require_invoice_key
|
||||||
|
from lnbits.settings import settings as lnbits_settings
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from .crud import get_account, get_user_permissions
|
||||||
|
from .models import PermissionType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuthContext:
|
||||||
|
"""
|
||||||
|
Authorization context for a request.
|
||||||
|
|
||||||
|
Contains all information needed to make authorization decisions.
|
||||||
|
Use this instead of directly accessing wallet/user properties scattered
|
||||||
|
throughout endpoint code.
|
||||||
|
"""
|
||||||
|
user_id: str
|
||||||
|
wallet_id: str
|
||||||
|
is_super_user: bool
|
||||||
|
wallet: WalletTypeInfo
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_admin(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if user is a Castle admin (super user).
|
||||||
|
|
||||||
|
Note: In Castle, admin = super_user. There's no separate admin concept.
|
||||||
|
"""
|
||||||
|
return self.is_super_user
|
||||||
|
|
||||||
|
def require_super_user(self) -> None:
|
||||||
|
"""Raise HTTPException if not super user."""
|
||||||
|
if not self.is_super_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail="Super user access required"
|
||||||
|
)
|
||||||
|
|
||||||
|
def require_self_or_super_user(self, target_user_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Require that user is accessing their own data or is super user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_user_id: The user ID being accessed
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If user is neither the target nor super user
|
||||||
|
"""
|
||||||
|
if not self.is_super_user and self.user_id != target_user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail="Access denied: you can only access your own data"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_auth_context(wallet: WalletTypeInfo) -> AuthContext:
|
||||||
|
"""Build AuthContext from wallet info."""
|
||||||
|
user_id = wallet.wallet.user
|
||||||
|
return AuthContext(
|
||||||
|
user_id=user_id,
|
||||||
|
wallet_id=wallet.wallet.id,
|
||||||
|
is_super_user=user_id == lnbits_settings.super_user,
|
||||||
|
wallet=wallet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===== FastAPI Dependencies =====
|
||||||
|
|
||||||
|
async def require_authenticated(
|
||||||
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
|
) -> AuthContext:
|
||||||
|
"""
|
||||||
|
Require authentication (invoice key minimum).
|
||||||
|
|
||||||
|
Returns AuthContext with user information.
|
||||||
|
Use for read-only access to user's own data.
|
||||||
|
"""
|
||||||
|
return _build_auth_context(wallet)
|
||||||
|
|
||||||
|
|
||||||
|
async def require_authenticated_write(
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> AuthContext:
|
||||||
|
"""
|
||||||
|
Require authentication with write permissions (admin key).
|
||||||
|
|
||||||
|
Returns AuthContext with user information.
|
||||||
|
Use for write operations on user's own data.
|
||||||
|
"""
|
||||||
|
return _build_auth_context(wallet)
|
||||||
|
|
||||||
|
|
||||||
|
async def require_super_user(
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> AuthContext:
|
||||||
|
"""
|
||||||
|
Require super user access.
|
||||||
|
|
||||||
|
Raises HTTPException 403 if not super user.
|
||||||
|
Use for Castle admin operations.
|
||||||
|
"""
|
||||||
|
auth = _build_auth_context(wallet)
|
||||||
|
if not auth.is_super_user:
|
||||||
|
logger.warning(
|
||||||
|
f"Super user access denied for user {auth.user_id[:8]} "
|
||||||
|
f"attempting admin operation"
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail="Super user access required"
|
||||||
|
)
|
||||||
|
return auth
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Resource Access Checks =====
|
||||||
|
|
||||||
|
async def can_access_account(
|
||||||
|
auth: AuthContext,
|
||||||
|
account_id: str,
|
||||||
|
permission_type: PermissionType = PermissionType.READ,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Check if user can access an account.
|
||||||
|
|
||||||
|
Access is granted if:
|
||||||
|
1. User is super user (full access)
|
||||||
|
2. User owns the account (user-specific accounts like Assets:Receivable:User-abc123)
|
||||||
|
3. User has explicit permission for the account
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auth: The authorization context
|
||||||
|
account_id: The account ID to check
|
||||||
|
permission_type: The type of access needed (READ, SUBMIT_EXPENSE, MANAGE)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if access is allowed, False otherwise
|
||||||
|
"""
|
||||||
|
# Super users have full access
|
||||||
|
if auth.is_super_user:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if this is the user's own account
|
||||||
|
account = await get_account(account_id)
|
||||||
|
if account:
|
||||||
|
user_short = auth.user_id[:8]
|
||||||
|
if f"User-{user_short}" in account.name:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check explicit permissions
|
||||||
|
permissions = await get_user_permissions(auth.user_id)
|
||||||
|
for perm in permissions:
|
||||||
|
if perm.account_id == account_id:
|
||||||
|
# Check if permission type is sufficient
|
||||||
|
if perm.permission_type == PermissionType.MANAGE:
|
||||||
|
return True # MANAGE grants all access
|
||||||
|
if perm.permission_type == permission_type:
|
||||||
|
return True
|
||||||
|
if (
|
||||||
|
permission_type == PermissionType.READ
|
||||||
|
and perm.permission_type in [PermissionType.SUBMIT_EXPENSE, PermissionType.MANAGE]
|
||||||
|
):
|
||||||
|
return True # Higher permissions include READ
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def require_account_access(
|
||||||
|
auth: AuthContext,
|
||||||
|
account_id: str,
|
||||||
|
permission_type: PermissionType = PermissionType.READ,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Require access to an account, raising HTTPException if denied.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auth: The authorization context
|
||||||
|
account_id: The account ID to check
|
||||||
|
permission_type: The type of access needed
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If access is denied
|
||||||
|
"""
|
||||||
|
if not await can_access_account(auth, account_id, permission_type):
|
||||||
|
logger.warning(
|
||||||
|
f"Account access denied: user {auth.user_id[:8]} "
|
||||||
|
f"attempted {permission_type.value} on account {account_id}"
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail=f"Access denied to account {account_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def can_access_user_data(auth: AuthContext, target_user_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if user can access another user's data.
|
||||||
|
|
||||||
|
Access is granted if:
|
||||||
|
1. User is super user
|
||||||
|
2. User is accessing their own data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auth: The authorization context
|
||||||
|
target_user_id: The user ID whose data is being accessed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if access is allowed
|
||||||
|
"""
|
||||||
|
if auth.is_super_user:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Users can access their own data - compare full ID or short ID
|
||||||
|
if auth.user_id == target_user_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Also allow if short IDs match (8 char prefix)
|
||||||
|
if auth.user_id[:8] == target_user_id[:8]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def require_user_data_access(
|
||||||
|
auth: AuthContext,
|
||||||
|
target_user_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Require access to a user's data, raising HTTPException if denied.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auth: The authorization context
|
||||||
|
target_user_id: The user ID whose data is being accessed
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If access is denied
|
||||||
|
"""
|
||||||
|
if not await can_access_user_data(auth, target_user_id):
|
||||||
|
logger.warning(
|
||||||
|
f"User data access denied: user {auth.user_id[:8]} "
|
||||||
|
f"attempted to access data for user {target_user_id[:8]}"
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail="Access denied: you can only access your own data"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Utility Functions =====
|
||||||
|
|
||||||
|
def get_user_id_from_wallet(wallet: WalletTypeInfo) -> str:
|
||||||
|
"""
|
||||||
|
Get user ID from wallet info.
|
||||||
|
|
||||||
|
IMPORTANT: Always use wallet.wallet.user (not wallet.wallet.id).
|
||||||
|
- wallet.wallet.user = the user's ID
|
||||||
|
- wallet.wallet.id = the wallet's ID (NOT the same!)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wallet: The wallet type info from LNbits
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The user ID
|
||||||
|
"""
|
||||||
|
return wallet.wallet.user
|
||||||
|
|
||||||
|
|
||||||
|
def is_super_user(user_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a user ID is the super user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user ID to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if this is the super user
|
||||||
|
"""
|
||||||
|
return user_id == lnbits_settings.super_user
|
||||||
293
views_api.py
293
views_api.py
|
|
@ -83,6 +83,14 @@ from .models import (
|
||||||
UserWithRoles,
|
UserWithRoles,
|
||||||
)
|
)
|
||||||
from .services import get_settings, get_user_wallet, update_settings, update_user_wallet
|
from .services import get_settings, get_user_wallet, update_settings, update_user_wallet
|
||||||
|
from .auth import (
|
||||||
|
AuthContext,
|
||||||
|
require_authenticated,
|
||||||
|
require_authenticated_write,
|
||||||
|
require_super_user,
|
||||||
|
require_account_access,
|
||||||
|
require_user_data_access,
|
||||||
|
)
|
||||||
|
|
||||||
castle_api_router = APIRouter()
|
castle_api_router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -276,26 +284,34 @@ async def api_get_accounts(
|
||||||
@castle_api_router.post("/api/v1/accounts", status_code=HTTPStatus.CREATED)
|
@castle_api_router.post("/api/v1/accounts", status_code=HTTPStatus.CREATED)
|
||||||
async def api_create_account(
|
async def api_create_account(
|
||||||
data: CreateAccount,
|
data: CreateAccount,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> Account:
|
) -> Account:
|
||||||
"""Create a new account (admin only)"""
|
"""Create a new account (super user only)"""
|
||||||
return await create_account(data)
|
return await create_account(data)
|
||||||
|
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/accounts/{account_id}")
|
@castle_api_router.get("/api/v1/accounts/{account_id}")
|
||||||
async def api_get_account(account_id: str) -> Account:
|
async def api_get_account(
|
||||||
"""Get a specific account"""
|
account_id: str,
|
||||||
|
auth: AuthContext = Depends(require_authenticated),
|
||||||
|
) -> Account:
|
||||||
|
"""Get a specific account (requires authentication and account access)"""
|
||||||
account = await get_account(account_id)
|
account = await get_account(account_id)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Account not found"
|
status_code=HTTPStatus.NOT_FOUND, detail="Account not found"
|
||||||
)
|
)
|
||||||
|
# Check access permission
|
||||||
|
await require_account_access(auth, account_id, PermissionType.READ)
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/accounts/{account_id}/balance")
|
@castle_api_router.get("/api/v1/accounts/{account_id}/balance")
|
||||||
async def api_get_account_balance(account_id: str) -> dict:
|
async def api_get_account_balance(
|
||||||
"""Get account balance from Fava/Beancount"""
|
account_id: str,
|
||||||
|
auth: AuthContext = Depends(require_authenticated),
|
||||||
|
) -> dict:
|
||||||
|
"""Get account balance from Fava/Beancount (requires authentication and account access)"""
|
||||||
from .fava_client import get_fava_client
|
from .fava_client import get_fava_client
|
||||||
|
|
||||||
# Get account to retrieve its name
|
# Get account to retrieve its name
|
||||||
|
|
@ -303,6 +319,9 @@ async def api_get_account_balance(account_id: str) -> dict:
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(status_code=404, detail="Account not found")
|
raise HTTPException(status_code=404, detail="Account not found")
|
||||||
|
|
||||||
|
# Check access permission
|
||||||
|
await require_account_access(auth, account_id, PermissionType.READ)
|
||||||
|
|
||||||
# Query Fava for balance
|
# Query Fava for balance
|
||||||
fava = get_fava_client()
|
fava = get_fava_client()
|
||||||
balance_data = await fava.get_account_balance(account.name)
|
balance_data = await fava.get_account_balance(account.name)
|
||||||
|
|
@ -316,11 +335,16 @@ async def api_get_account_balance(account_id: str) -> dict:
|
||||||
|
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/accounts/{account_id}/transactions")
|
@castle_api_router.get("/api/v1/accounts/{account_id}/transactions")
|
||||||
async def api_get_account_transactions(account_id: str, limit: int = 100) -> list[dict]:
|
async def api_get_account_transactions(
|
||||||
|
account_id: str,
|
||||||
|
limit: int = 100,
|
||||||
|
auth: AuthContext = Depends(require_authenticated),
|
||||||
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Get all transactions for an account from Fava/Beancount.
|
Get all transactions for an account from Fava/Beancount.
|
||||||
|
|
||||||
Returns transactions affecting this account in reverse chronological order.
|
Returns transactions affecting this account in reverse chronological order.
|
||||||
|
Requires authentication and account access.
|
||||||
"""
|
"""
|
||||||
from .fava_client import get_fava_client
|
from .fava_client import get_fava_client
|
||||||
|
|
||||||
|
|
@ -332,6 +356,9 @@ async def api_get_account_transactions(account_id: str, limit: int = 100) -> lis
|
||||||
detail=f"Account {account_id} not found"
|
detail=f"Account {account_id} not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check access permission
|
||||||
|
await require_account_access(auth, account_id, PermissionType.READ)
|
||||||
|
|
||||||
# Query Fava for transactions
|
# Query Fava for transactions
|
||||||
fava = get_fava_client()
|
fava = get_fava_client()
|
||||||
transactions = await fava.get_account_transactions(account.name, limit)
|
transactions = await fava.get_account_transactions(account.name, limit)
|
||||||
|
|
@ -343,11 +370,15 @@ async def api_get_account_transactions(account_id: str, limit: int = 100) -> lis
|
||||||
|
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/entries")
|
@castle_api_router.get("/api/v1/entries")
|
||||||
async def api_get_journal_entries(limit: int = 100) -> list[dict]:
|
async def api_get_journal_entries(
|
||||||
|
limit: int = 100,
|
||||||
|
auth: AuthContext = Depends(require_super_user),
|
||||||
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Get all journal entries from Fava/Beancount.
|
Get all journal entries from Fava/Beancount.
|
||||||
|
|
||||||
Returns all transactions in reverse chronological order with username enrichment.
|
Returns all transactions in reverse chronological order with username enrichment.
|
||||||
|
SUPER USER ONLY - exposes all transaction data.
|
||||||
"""
|
"""
|
||||||
from lnbits.core.crud.users import get_user
|
from lnbits.core.crud.users import get_user
|
||||||
from .fava_client import get_fava_client
|
from .fava_client import get_fava_client
|
||||||
|
|
@ -721,22 +752,15 @@ async def _get_username_from_user_id(user_id: str) -> str:
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/entries/pending")
|
@castle_api_router.get("/api/v1/entries/pending")
|
||||||
async def api_get_pending_entries(
|
async def api_get_pending_entries(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Get all pending expense entries that need approval (admin only).
|
Get all pending expense entries that need approval (super user only).
|
||||||
|
|
||||||
Returns transactions with flag='!' from Fava/Beancount.
|
Returns transactions with flag='!' from Fava/Beancount.
|
||||||
"""
|
"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
from .fava_client import get_fava_client
|
from .fava_client import get_fava_client
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can access this endpoint",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Query Fava for all journal entries (includes links, tags, full metadata)
|
# Query Fava for all journal entries (includes links, tags, full metadata)
|
||||||
fava = get_fava_client()
|
fava = get_fava_client()
|
||||||
all_entries = await fava.get_journal_entries()
|
all_entries = await fava.get_journal_entries()
|
||||||
|
|
@ -949,7 +973,7 @@ async def api_create_journal_entry(
|
||||||
# Entry metadata (excluding tags and links which go at transaction level)
|
# Entry metadata (excluding tags and links which go at transaction level)
|
||||||
entry_meta = {k: v for k, v in data.meta.items() if k not in ["tags", "links"]}
|
entry_meta = {k: v for k, v in data.meta.items() if k not in ["tags", "links"]}
|
||||||
entry_meta["source"] = "castle-api"
|
entry_meta["source"] = "castle-api"
|
||||||
entry_meta["created-by"] = wallet.wallet.id
|
entry_meta["created-by"] = wallet.wallet.user # Use user_id, not wallet_id
|
||||||
|
|
||||||
# Format as Beancount entry
|
# Format as Beancount entry
|
||||||
fava = get_fava_client()
|
fava = get_fava_client()
|
||||||
|
|
@ -975,7 +999,7 @@ async def api_create_journal_entry(
|
||||||
id=f"fava-{timestamp}",
|
id=f"fava-{timestamp}",
|
||||||
description=data.description,
|
description=data.description,
|
||||||
entry_date=data.entry_date if data.entry_date else datetime.now(),
|
entry_date=data.entry_date if data.entry_date else datetime.now(),
|
||||||
created_by=wallet.wallet.id,
|
created_by=wallet.wallet.user, # Use user_id, not wallet_id
|
||||||
created_at=datetime.now(),
|
created_at=datetime.now(),
|
||||||
reference=data.reference,
|
reference=data.reference,
|
||||||
flag=data.flag if data.flag else JournalEntryFlag.CLEARED,
|
flag=data.flag if data.flag else JournalEntryFlag.CLEARED,
|
||||||
|
|
@ -1138,7 +1162,7 @@ async def api_create_expense_entry(
|
||||||
id=entry_id, # Use the generated castle entry ID
|
id=entry_id, # Use the generated castle entry ID
|
||||||
description=data.description + description_suffix,
|
description=data.description + description_suffix,
|
||||||
entry_date=data.entry_date if data.entry_date else datetime.now(),
|
entry_date=data.entry_date if data.entry_date else datetime.now(),
|
||||||
created_by=wallet.wallet.id,
|
created_by=wallet.wallet.user, # Use user_id, not wallet_id
|
||||||
created_at=datetime.now(),
|
created_at=datetime.now(),
|
||||||
reference=castle_reference,
|
reference=castle_reference,
|
||||||
flag=JournalEntryFlag.PENDING,
|
flag=JournalEntryFlag.PENDING,
|
||||||
|
|
@ -1266,7 +1290,7 @@ async def api_create_receivable_entry(
|
||||||
id=entry_id, # Use the generated castle entry ID
|
id=entry_id, # Use the generated castle entry ID
|
||||||
description=data.description + description_suffix,
|
description=data.description + description_suffix,
|
||||||
entry_date=datetime.now(),
|
entry_date=datetime.now(),
|
||||||
created_by=wallet.wallet.id,
|
created_by=wallet.wallet.user, # Use user_id, not wallet_id
|
||||||
created_at=datetime.now(),
|
created_at=datetime.now(),
|
||||||
reference=castle_reference, # Use castle reference with unique ID
|
reference=castle_reference, # Use castle reference with unique ID
|
||||||
flag=JournalEntryFlag.PENDING,
|
flag=JournalEntryFlag.PENDING,
|
||||||
|
|
@ -1380,7 +1404,7 @@ async def api_create_revenue_entry(
|
||||||
id=entry_id,
|
id=entry_id,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
entry_date=datetime.now(),
|
entry_date=datetime.now(),
|
||||||
created_by=wallet.wallet.id,
|
created_by=wallet.wallet.user, # Use user_id, not wallet_id
|
||||||
created_at=datetime.now(),
|
created_at=datetime.now(),
|
||||||
reference=castle_reference,
|
reference=castle_reference,
|
||||||
flag=JournalEntryFlag.CLEARED,
|
flag=JournalEntryFlag.CLEARED,
|
||||||
|
|
@ -1444,8 +1468,18 @@ async def api_get_my_balance(
|
||||||
|
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/balance/{user_id}")
|
@castle_api_router.get("/api/v1/balance/{user_id}")
|
||||||
async def api_get_user_balance(user_id: str) -> UserBalance:
|
async def api_get_user_balance(
|
||||||
"""Get a specific user's balance with the Castle (from Fava/Beancount)"""
|
user_id: str,
|
||||||
|
auth: AuthContext = Depends(require_authenticated),
|
||||||
|
) -> UserBalance:
|
||||||
|
"""
|
||||||
|
Get a specific user's balance with the Castle (from Fava/Beancount).
|
||||||
|
|
||||||
|
Users can only access their own balance. Super users can access any user's balance.
|
||||||
|
"""
|
||||||
|
# Check access: must be own data or super user
|
||||||
|
await require_user_data_access(auth, user_id)
|
||||||
|
|
||||||
from .fava_client import get_fava_client
|
from .fava_client import get_fava_client
|
||||||
|
|
||||||
fava = get_fava_client()
|
fava = get_fava_client()
|
||||||
|
|
@ -1461,9 +1495,9 @@ async def api_get_user_balance(user_id: str) -> UserBalance:
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/balances/all")
|
@castle_api_router.get("/api/v1/balances/all")
|
||||||
async def api_get_all_balances(
|
async def api_get_all_balances(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Get all user balances (admin/super user only) from Fava/Beancount"""
|
"""Get all user balances (super user only) from Fava/Beancount"""
|
||||||
from .fava_client import get_fava_client
|
from .fava_client import get_fava_client
|
||||||
|
|
||||||
fava = get_fava_client()
|
fava = get_fava_client()
|
||||||
|
|
@ -1802,7 +1836,7 @@ async def api_pay_user(
|
||||||
@castle_api_router.post("/api/v1/receivables/settle")
|
@castle_api_router.post("/api/v1/receivables/settle")
|
||||||
async def api_settle_receivable(
|
async def api_settle_receivable(
|
||||||
data: SettleReceivable,
|
data: SettleReceivable,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Manually settle a receivable (record when user pays castle in person).
|
Manually settle a receivable (record when user pays castle in person).
|
||||||
|
|
@ -1812,15 +1846,8 @@ async def api_settle_receivable(
|
||||||
- Bank transfers
|
- Bank transfers
|
||||||
- Other manual settlements
|
- Other manual settlements
|
||||||
|
|
||||||
Admin only.
|
Super user only.
|
||||||
"""
|
"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can settle receivables",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate payment method
|
# Validate payment method
|
||||||
valid_methods = ["cash", "bank_transfer", "check", "lightning", "btc_onchain", "other"]
|
valid_methods = ["cash", "bank_transfer", "check", "lightning", "btc_onchain", "other"]
|
||||||
|
|
@ -1957,7 +1984,7 @@ async def api_settle_receivable(
|
||||||
@castle_api_router.post("/api/v1/payables/pay")
|
@castle_api_router.post("/api/v1/payables/pay")
|
||||||
async def api_pay_user(
|
async def api_pay_user(
|
||||||
data: PayUser,
|
data: PayUser,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Pay a user (castle pays user for expense/liability).
|
Pay a user (castle pays user for expense/liability).
|
||||||
|
|
@ -1966,15 +1993,8 @@ async def api_pay_user(
|
||||||
- Lightning payments: already executed, just record the payment
|
- Lightning payments: already executed, just record the payment
|
||||||
- Cash/Bank/Check: record manual payment that was made
|
- Cash/Bank/Check: record manual payment that was made
|
||||||
|
|
||||||
Admin only.
|
Super user only.
|
||||||
"""
|
"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can pay users",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate payment method
|
# Validate payment method
|
||||||
valid_methods = ["cash", "bank_transfer", "check", "lightning", "btc_onchain", "other"]
|
valid_methods = ["cash", "bank_transfer", "check", "lightning", "btc_onchain", "other"]
|
||||||
|
|
@ -2152,16 +2172,9 @@ async def api_update_settings(
|
||||||
@castle_api_router.get("/api/v1/user-wallet/{user_id}")
|
@castle_api_router.get("/api/v1/user-wallet/{user_id}")
|
||||||
async def api_get_user_wallet(
|
async def api_get_user_wallet(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Get user's wallet settings (admin only)"""
|
"""Get user's wallet settings (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can access user wallet info",
|
|
||||||
)
|
|
||||||
|
|
||||||
user_wallet = await get_user_wallet(user_id)
|
user_wallet = await get_user_wallet(user_id)
|
||||||
if not user_wallet:
|
if not user_wallet:
|
||||||
|
|
@ -2183,9 +2196,9 @@ async def api_get_user_wallet(
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/users")
|
@castle_api_router.get("/api/v1/users")
|
||||||
async def api_get_all_users(
|
async def api_get_all_users(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Get all users who have configured their wallet (admin only)"""
|
"""Get all users who have configured their wallet (super user only)"""
|
||||||
from lnbits.core.crud.users import get_user
|
from lnbits.core.crud.users import get_user
|
||||||
|
|
||||||
user_settings = await get_all_user_wallet_settings()
|
user_settings = await get_all_user_wallet_settings()
|
||||||
|
|
@ -2209,12 +2222,12 @@ async def api_get_all_users(
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/admin/castle-users")
|
@castle_api_router.get("/api/v1/admin/castle-users")
|
||||||
async def api_get_castle_users(
|
async def api_get_castle_users(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Get all users who have configured their wallet in Castle.
|
Get all users who have configured their wallet in Castle.
|
||||||
These are users who can interact with Castle (submit expenses, receive permissions, etc.).
|
These are users who can interact with Castle (submit expenses, receive permissions, etc.).
|
||||||
Admin only.
|
Super user only.
|
||||||
"""
|
"""
|
||||||
from lnbits.core.crud.users import get_user
|
from lnbits.core.crud.users import get_user
|
||||||
|
|
||||||
|
|
@ -2247,10 +2260,10 @@ async def api_expense_report(
|
||||||
start_date: Optional[str] = None,
|
start_date: Optional[str] = None,
|
||||||
end_date: Optional[str] = None,
|
end_date: Optional[str] = None,
|
||||||
group_by: str = "account",
|
group_by: str = "account",
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Get expense summary report using BQL.
|
Get expense summary report using BQL. Super user only.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
start_date: Filter from this date (YYYY-MM-DD), optional
|
start_date: Filter from this date (YYYY-MM-DD), optional
|
||||||
|
|
@ -2511,32 +2524,18 @@ async def api_get_manual_payment_requests(
|
||||||
@castle_api_router.get("/api/v1/manual-payment-requests/all")
|
@castle_api_router.get("/api/v1/manual-payment-requests/all")
|
||||||
async def api_get_all_manual_payment_requests(
|
async def api_get_all_manual_payment_requests(
|
||||||
status: str = None,
|
status: str = None,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> list[ManualPaymentRequest]:
|
) -> list[ManualPaymentRequest]:
|
||||||
"""Get all manual payment requests (Castle admin only)"""
|
"""Get all manual payment requests (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can access this endpoint",
|
|
||||||
)
|
|
||||||
return await get_all_manual_payment_requests(status)
|
return await get_all_manual_payment_requests(status)
|
||||||
|
|
||||||
|
|
||||||
@castle_api_router.post("/api/v1/manual-payment-requests/{request_id}/approve")
|
@castle_api_router.post("/api/v1/manual-payment-requests/{request_id}/approve")
|
||||||
async def api_approve_manual_payment_request(
|
async def api_approve_manual_payment_request(
|
||||||
request_id: str,
|
request_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> ManualPaymentRequest:
|
) -> ManualPaymentRequest:
|
||||||
"""Approve a manual payment request and create accounting entry (Castle admin only)"""
|
"""Approve a manual payment request and create accounting entry (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can access this endpoint",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the request
|
# Get the request
|
||||||
request = await get_manual_payment_request(request_id)
|
request = await get_manual_payment_request(request_id)
|
||||||
|
|
@ -2604,17 +2603,9 @@ async def api_approve_manual_payment_request(
|
||||||
@castle_api_router.post("/api/v1/manual-payment-requests/{request_id}/reject")
|
@castle_api_router.post("/api/v1/manual-payment-requests/{request_id}/reject")
|
||||||
async def api_reject_manual_payment_request(
|
async def api_reject_manual_payment_request(
|
||||||
request_id: str,
|
request_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> ManualPaymentRequest:
|
) -> ManualPaymentRequest:
|
||||||
"""Reject a manual payment request (Castle admin only)"""
|
"""Reject a manual payment request (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can access this endpoint",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the request
|
# Get the request
|
||||||
request = await get_manual_payment_request(request_id)
|
request = await get_manual_payment_request(request_id)
|
||||||
if not request:
|
if not request:
|
||||||
|
|
@ -2629,7 +2620,7 @@ async def api_reject_manual_payment_request(
|
||||||
detail=f"Request already {request.status}",
|
detail=f"Request already {request.status}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return await reject_manual_payment_request(request_id, wallet.wallet.user)
|
return await reject_manual_payment_request(request_id, auth.user_id)
|
||||||
|
|
||||||
|
|
||||||
# ===== EXPENSE APPROVAL ENDPOINTS =====
|
# ===== EXPENSE APPROVAL ENDPOINTS =====
|
||||||
|
|
@ -2638,22 +2629,15 @@ async def api_reject_manual_payment_request(
|
||||||
@castle_api_router.post("/api/v1/entries/{entry_id}/approve")
|
@castle_api_router.post("/api/v1/entries/{entry_id}/approve")
|
||||||
async def api_approve_expense_entry(
|
async def api_approve_expense_entry(
|
||||||
entry_id: str,
|
entry_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Approve a pending expense entry by changing flag from '!' to '*' (admin only).
|
Approve a pending expense entry by changing flag from '!' to '*' (super user only).
|
||||||
|
|
||||||
This updates the transaction in the Beancount file via Fava API.
|
This updates the transaction in the Beancount file via Fava API.
|
||||||
"""
|
"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
from .fava_client import get_fava_client
|
from .fava_client import get_fava_client
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can approve expenses",
|
|
||||||
)
|
|
||||||
|
|
||||||
fava = get_fava_client()
|
fava = get_fava_client()
|
||||||
|
|
||||||
# 1. Get all journal entries from Fava
|
# 1. Get all journal entries from Fava
|
||||||
|
|
@ -2725,23 +2709,16 @@ async def api_approve_expense_entry(
|
||||||
@castle_api_router.post("/api/v1/entries/{entry_id}/reject")
|
@castle_api_router.post("/api/v1/entries/{entry_id}/reject")
|
||||||
async def api_reject_expense_entry(
|
async def api_reject_expense_entry(
|
||||||
entry_id: str,
|
entry_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Reject a pending expense entry by marking it as voided (admin only).
|
Reject a pending expense entry by marking it as voided (super user only).
|
||||||
|
|
||||||
Adds #voided tag for audit trail while keeping the '!' flag.
|
Adds #voided tag for audit trail while keeping the '!' flag.
|
||||||
Voided transactions are excluded from balances but preserved in the ledger.
|
Voided transactions are excluded from balances but preserved in the ledger.
|
||||||
"""
|
"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
from .fava_client import get_fava_client
|
from .fava_client import get_fava_client
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can reject expenses",
|
|
||||||
)
|
|
||||||
|
|
||||||
fava = get_fava_client()
|
fava = get_fava_client()
|
||||||
|
|
||||||
# 1. Get all journal entries from Fava
|
# 1. Get all journal entries from Fava
|
||||||
|
|
@ -2823,10 +2800,10 @@ async def api_reject_expense_entry(
|
||||||
@castle_api_router.post("/api/v1/assertions")
|
@castle_api_router.post("/api/v1/assertions")
|
||||||
async def api_create_balance_assertion(
|
async def api_create_balance_assertion(
|
||||||
data: CreateBalanceAssertion,
|
data: CreateBalanceAssertion,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> BalanceAssertion:
|
) -> BalanceAssertion:
|
||||||
"""
|
"""
|
||||||
Create a balance assertion for reconciliation (admin only).
|
Create a balance assertion for reconciliation (super user only).
|
||||||
|
|
||||||
Uses hybrid approach:
|
Uses hybrid approach:
|
||||||
1. Writes balance assertion to Beancount (via Fava) - source of truth
|
1. Writes balance assertion to Beancount (via Fava) - source of truth
|
||||||
|
|
@ -2835,16 +2812,9 @@ async def api_create_balance_assertion(
|
||||||
|
|
||||||
The assertion will be checked immediately upon creation.
|
The assertion will be checked immediately upon creation.
|
||||||
"""
|
"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
from .fava_client import get_fava_client
|
from .fava_client import get_fava_client
|
||||||
from .beancount_format import format_balance
|
from .beancount_format import format_balance
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can create balance assertions",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify account exists
|
# Verify account exists
|
||||||
account = await get_account(data.account_id)
|
account = await get_account(data.account_id)
|
||||||
if not account:
|
if not account:
|
||||||
|
|
@ -2876,7 +2846,7 @@ async def api_create_balance_assertion(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store metadata in Castle DB for UI convenience
|
# Store metadata in Castle DB for UI convenience
|
||||||
assertion = await create_balance_assertion(data, wallet.wallet.user)
|
assertion = await create_balance_assertion(data, auth.user_id)
|
||||||
|
|
||||||
# Check it immediately (queries Fava for actual balance)
|
# Check it immediately (queries Fava for actual balance)
|
||||||
try:
|
try:
|
||||||
|
|
@ -2911,16 +2881,9 @@ async def api_get_balance_assertions(
|
||||||
account_id: str = None,
|
account_id: str = None,
|
||||||
status: str = None,
|
status: str = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> list[BalanceAssertion]:
|
) -> list[BalanceAssertion]:
|
||||||
"""Get balance assertions with optional filters (admin only)"""
|
"""Get balance assertions with optional filters (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can view balance assertions",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Parse status enum if provided
|
# Parse status enum if provided
|
||||||
status_enum = None
|
status_enum = None
|
||||||
|
|
@ -2943,17 +2906,9 @@ async def api_get_balance_assertions(
|
||||||
@castle_api_router.get("/api/v1/assertions/{assertion_id}")
|
@castle_api_router.get("/api/v1/assertions/{assertion_id}")
|
||||||
async def api_get_balance_assertion(
|
async def api_get_balance_assertion(
|
||||||
assertion_id: str,
|
assertion_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> BalanceAssertion:
|
) -> BalanceAssertion:
|
||||||
"""Get a specific balance assertion (admin only)"""
|
"""Get a specific balance assertion (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can view balance assertions",
|
|
||||||
)
|
|
||||||
|
|
||||||
assertion = await get_balance_assertion(assertion_id)
|
assertion = await get_balance_assertion(assertion_id)
|
||||||
if not assertion:
|
if not assertion:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -2967,17 +2922,9 @@ async def api_get_balance_assertion(
|
||||||
@castle_api_router.post("/api/v1/assertions/{assertion_id}/check")
|
@castle_api_router.post("/api/v1/assertions/{assertion_id}/check")
|
||||||
async def api_check_balance_assertion(
|
async def api_check_balance_assertion(
|
||||||
assertion_id: str,
|
assertion_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> BalanceAssertion:
|
) -> BalanceAssertion:
|
||||||
"""Re-check a balance assertion (admin only)"""
|
"""Re-check a balance assertion (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can check balance assertions",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
assertion = await check_balance_assertion(assertion_id)
|
assertion = await check_balance_assertion(assertion_id)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
@ -2992,17 +2939,9 @@ async def api_check_balance_assertion(
|
||||||
@castle_api_router.delete("/api/v1/assertions/{assertion_id}")
|
@castle_api_router.delete("/api/v1/assertions/{assertion_id}")
|
||||||
async def api_delete_balance_assertion(
|
async def api_delete_balance_assertion(
|
||||||
assertion_id: str,
|
assertion_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Delete a balance assertion (admin only)"""
|
"""Delete a balance assertion (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can delete balance assertions",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify it exists
|
# Verify it exists
|
||||||
assertion = await get_balance_assertion(assertion_id)
|
assertion = await get_balance_assertion(assertion_id)
|
||||||
if not assertion:
|
if not assertion:
|
||||||
|
|
@ -3021,16 +2960,9 @@ async def api_delete_balance_assertion(
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/reconciliation/summary")
|
@castle_api_router.get("/api/v1/reconciliation/summary")
|
||||||
async def api_get_reconciliation_summary(
|
async def api_get_reconciliation_summary(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Get reconciliation summary (admin only)"""
|
"""Get reconciliation summary (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can access reconciliation",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get all assertions
|
# Get all assertions
|
||||||
all_assertions = await get_balance_assertions(limit=1000)
|
all_assertions = await get_balance_assertions(limit=1000)
|
||||||
|
|
@ -3079,16 +3011,9 @@ async def api_get_reconciliation_summary(
|
||||||
|
|
||||||
@castle_api_router.post("/api/v1/reconciliation/check-all")
|
@castle_api_router.post("/api/v1/reconciliation/check-all")
|
||||||
async def api_check_all_assertions(
|
async def api_check_all_assertions(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Re-check all balance assertions (admin only)"""
|
"""Re-check all balance assertions (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can run reconciliation checks",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get all assertions
|
# Get all assertions
|
||||||
all_assertions = await get_balance_assertions(limit=1000)
|
all_assertions = await get_balance_assertions(limit=1000)
|
||||||
|
|
@ -3117,16 +3042,9 @@ async def api_check_all_assertions(
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/reconciliation/discrepancies")
|
@castle_api_router.get("/api/v1/reconciliation/discrepancies")
|
||||||
async def api_get_discrepancies(
|
async def api_get_discrepancies(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Get all discrepancies (failed assertions, flagged entries) (admin only)"""
|
"""Get all discrepancies (failed assertions, flagged entries) (super user only)"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can view discrepancies",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get failed assertions
|
# Get failed assertions
|
||||||
failed_assertions = await get_balance_assertions(
|
failed_assertions = await get_balance_assertions(
|
||||||
|
|
@ -3154,21 +3072,14 @@ async def api_get_discrepancies(
|
||||||
|
|
||||||
@castle_api_router.post("/api/v1/tasks/daily-reconciliation")
|
@castle_api_router.post("/api/v1/tasks/daily-reconciliation")
|
||||||
async def api_run_daily_reconciliation(
|
async def api_run_daily_reconciliation(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auth: AuthContext = Depends(require_super_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Manually trigger the daily reconciliation check (admin only).
|
Manually trigger the daily reconciliation check (super user only).
|
||||||
This endpoint can also be called via cron job.
|
This endpoint can also be called via cron job.
|
||||||
|
|
||||||
Returns a summary of the reconciliation check results.
|
Returns a summary of the reconciliation check results.
|
||||||
"""
|
"""
|
||||||
from lnbits.settings import settings as lnbits_settings
|
|
||||||
|
|
||||||
if wallet.wallet.user != lnbits_settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Only super user can run daily reconciliation",
|
|
||||||
)
|
|
||||||
|
|
||||||
from .tasks import check_all_balance_assertions
|
from .tasks import check_all_balance_assertions
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue