diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..b729347 --- /dev/null +++ b/auth.py @@ -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 diff --git a/views_api.py b/views_api.py index a3e206d..77eadaf 100644 --- a/views_api.py +++ b/views_api.py @@ -83,6 +83,14 @@ from .models import ( UserWithRoles, ) 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() @@ -276,26 +284,34 @@ async def api_get_accounts( @castle_api_router.post("/api/v1/accounts", status_code=HTTPStatus.CREATED) async def api_create_account( data: CreateAccount, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> Account: - """Create a new account (admin only)""" + """Create a new account (super user only)""" return await create_account(data) @castle_api_router.get("/api/v1/accounts/{account_id}") -async def api_get_account(account_id: str) -> Account: - """Get a specific account""" +async def api_get_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) if not account: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Account not found" ) + # Check access permission + await require_account_access(auth, account_id, PermissionType.READ) return account @castle_api_router.get("/api/v1/accounts/{account_id}/balance") -async def api_get_account_balance(account_id: str) -> dict: - """Get account balance from Fava/Beancount""" +async def api_get_account_balance( + 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 # Get account to retrieve its name @@ -303,6 +319,9 @@ async def api_get_account_balance(account_id: str) -> dict: if not account: 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 fava = get_fava_client() 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") -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. Returns transactions affecting this account in reverse chronological order. + Requires authentication and account access. """ 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" ) + # Check access permission + await require_account_access(auth, account_id, PermissionType.READ) + # Query Fava for transactions fava = get_fava_client() 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") -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. 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 .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") async def api_get_pending_entries( - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> 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. """ - from lnbits.settings import settings as lnbits_settings 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) fava = get_fava_client() 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_meta = {k: v for k, v in data.meta.items() if k not in ["tags", "links"]} 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 fava = get_fava_client() @@ -975,7 +999,7 @@ async def api_create_journal_entry( id=f"fava-{timestamp}", description=data.description, 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(), reference=data.reference, 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 description=data.description + description_suffix, 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(), reference=castle_reference, flag=JournalEntryFlag.PENDING, @@ -1266,7 +1290,7 @@ async def api_create_receivable_entry( id=entry_id, # Use the generated castle entry ID description=data.description + description_suffix, entry_date=datetime.now(), - created_by=wallet.wallet.id, + created_by=wallet.wallet.user, # Use user_id, not wallet_id created_at=datetime.now(), reference=castle_reference, # Use castle reference with unique ID flag=JournalEntryFlag.PENDING, @@ -1380,7 +1404,7 @@ async def api_create_revenue_entry( id=entry_id, description=data.description, entry_date=datetime.now(), - created_by=wallet.wallet.id, + created_by=wallet.wallet.user, # Use user_id, not wallet_id created_at=datetime.now(), reference=castle_reference, flag=JournalEntryFlag.CLEARED, @@ -1444,8 +1468,18 @@ async def api_get_my_balance( @castle_api_router.get("/api/v1/balance/{user_id}") -async def api_get_user_balance(user_id: str) -> UserBalance: - """Get a specific user's balance with the Castle (from Fava/Beancount)""" +async def api_get_user_balance( + 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 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") async def api_get_all_balances( - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> 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 fava = get_fava_client() @@ -1802,7 +1836,7 @@ async def api_pay_user( @castle_api_router.post("/api/v1/receivables/settle") async def api_settle_receivable( data: SettleReceivable, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> dict: """ Manually settle a receivable (record when user pays castle in person). @@ -1812,15 +1846,8 @@ async def api_settle_receivable( - Bank transfers - 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 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") async def api_pay_user( data: PayUser, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> dict: """ 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 - 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 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}") async def api_get_user_wallet( user_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> dict: - """Get user's wallet settings (admin 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", - ) + """Get user's wallet settings (super user only)""" user_wallet = await get_user_wallet(user_id) if not user_wallet: @@ -2183,9 +2196,9 @@ async def api_get_user_wallet( @castle_api_router.get("/api/v1/users") async def api_get_all_users( - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> 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 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") async def api_get_castle_users( - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> list[dict]: """ Get all users who have configured their wallet in Castle. 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 @@ -2247,10 +2260,10 @@ async def api_expense_report( start_date: Optional[str] = None, end_date: Optional[str] = None, group_by: str = "account", - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> dict: """ - Get expense summary report using BQL. + Get expense summary report using BQL. Super user only. Args: 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") async def api_get_all_manual_payment_requests( status: str = None, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> list[ManualPaymentRequest]: - """Get all manual payment requests (Castle admin 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 all manual payment requests (super user only)""" return await get_all_manual_payment_requests(status) @castle_api_router.post("/api/v1/manual-payment-requests/{request_id}/approve") async def api_approve_manual_payment_request( request_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> ManualPaymentRequest: - """Approve a manual payment request and create accounting entry (Castle admin 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", - ) + """Approve a manual payment request and create accounting entry (super user only)""" # Get the request 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") async def api_reject_manual_payment_request( request_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> ManualPaymentRequest: - """Reject a manual payment request (Castle admin 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", - ) - + """Reject a manual payment request (super user only)""" # Get the request request = await get_manual_payment_request(request_id) if not request: @@ -2629,7 +2620,7 @@ async def api_reject_manual_payment_request( 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 ===== @@ -2638,22 +2629,15 @@ async def api_reject_manual_payment_request( @castle_api_router.post("/api/v1/entries/{entry_id}/approve") async def api_approve_expense_entry( entry_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> 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. """ - from lnbits.settings import settings as lnbits_settings 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() # 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") async def api_reject_expense_entry( entry_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> 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. 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 - 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() # 1. Get all journal entries from Fava @@ -2823,10 +2800,10 @@ async def api_reject_expense_entry( @castle_api_router.post("/api/v1/assertions") async def api_create_balance_assertion( data: CreateBalanceAssertion, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> BalanceAssertion: """ - Create a balance assertion for reconciliation (admin only). + Create a balance assertion for reconciliation (super user only). Uses hybrid approach: 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. """ - from lnbits.settings import settings as lnbits_settings from .fava_client import get_fava_client 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 account = await get_account(data.account_id) if not account: @@ -2876,7 +2846,7 @@ async def api_create_balance_assertion( ) # 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) try: @@ -2911,16 +2881,9 @@ async def api_get_balance_assertions( account_id: str = None, status: str = None, limit: int = 100, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> list[BalanceAssertion]: - """Get balance assertions with optional filters (admin 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", - ) + """Get balance assertions with optional filters (super user only)""" # Parse status enum if provided status_enum = None @@ -2943,17 +2906,9 @@ async def api_get_balance_assertions( @castle_api_router.get("/api/v1/assertions/{assertion_id}") async def api_get_balance_assertion( assertion_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> BalanceAssertion: - """Get a specific balance assertion (admin 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", - ) - + """Get a specific balance assertion (super user only)""" assertion = await get_balance_assertion(assertion_id) if not assertion: raise HTTPException( @@ -2967,17 +2922,9 @@ async def api_get_balance_assertion( @castle_api_router.post("/api/v1/assertions/{assertion_id}/check") async def api_check_balance_assertion( assertion_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> BalanceAssertion: - """Re-check a balance assertion (admin 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", - ) - + """Re-check a balance assertion (super user only)""" try: assertion = await check_balance_assertion(assertion_id) except ValueError as e: @@ -2992,17 +2939,9 @@ async def api_check_balance_assertion( @castle_api_router.delete("/api/v1/assertions/{assertion_id}") async def api_delete_balance_assertion( assertion_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> dict: - """Delete a balance assertion (admin 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", - ) - + """Delete a balance assertion (super user only)""" # Verify it exists assertion = await get_balance_assertion(assertion_id) if not assertion: @@ -3021,16 +2960,9 @@ async def api_delete_balance_assertion( @castle_api_router.get("/api/v1/reconciliation/summary") async def api_get_reconciliation_summary( - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> dict: - """Get reconciliation summary (admin 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 reconciliation summary (super user only)""" # Get all assertions 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") async def api_check_all_assertions( - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> dict: - """Re-check all balance assertions (admin 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", - ) + """Re-check all balance assertions (super user only)""" # Get all assertions 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") async def api_get_discrepancies( - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> dict: - """Get all discrepancies (failed assertions, flagged entries) (admin 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 all discrepancies (failed assertions, flagged entries) (super user only)""" # Get failed 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") async def api_run_daily_reconciliation( - wallet: WalletTypeInfo = Depends(require_admin_key), + auth: AuthContext = Depends(require_super_user), ) -> 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. 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