+
Select the wallet that will be used for Castle operations and transactions.
+
+
+
Fava/Beancount Integration
+
+
+
+
+
+
+
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"]
@@ -2093,7 +2113,7 @@ async def api_pay_user(
if "meta" not in entry:
entry["meta"] = {}
entry["meta"]["payment-method"] = data.payment_method
- entry["meta"]["paid-by"] = wallet.wallet.user
+ entry["meta"]["paid-by"] = auth.user_id
if data.txid:
entry["meta"]["txid"] = data.txid
@@ -2152,19 +2172,26 @@ 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
+ """Get user's wallet settings (super user only)
- if wallet.wallet.user != lnbits_settings.super_user:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail="Only super user can access user wallet info",
- )
+ Supports both full UUIDs and truncated 8-char IDs (from Beancount accounts).
+ """
+ from .crud import get_user_wallet_settings_by_prefix
+ # First try exact match
user_wallet = await get_user_wallet(user_id)
- if not user_wallet:
+
+ # If not found and user_id looks like a truncated ID (8 chars), try prefix match
+ if not user_wallet or not user_wallet.user_wallet_id:
+ if len(user_id) <= 8:
+ stored_wallet = await get_user_wallet_settings_by_prefix(user_id)
+ if stored_wallet and stored_wallet.user_wallet_id:
+ user_wallet = stored_wallet
+ user_id = stored_wallet.id # Use the full ID
+
+ if not user_wallet or not user_wallet.user_wallet_id:
return {"user_id": user_id, "user_wallet_id": None}
# Get invoice key for the user's wallet (needed to generate invoices)
@@ -2183,9 +2210,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 +2236,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 +2274,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 +2538,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 +2617,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 +2634,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,29 +2643,22 @@ 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
+ import httpx
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
all_entries = await fava.get_journal_entries()
# 2. Find the entry with matching castle ID in links
- target_entry_hash = None
target_entry = None
for entry in all_entries:
@@ -2672,51 +2670,86 @@ async def api_approve_expense_entry(
link_clean = link.lstrip('^')
# Check if this entry has our castle ID
if link_clean == f"castle-{entry_id}" or link_clean.endswith(f"-{entry_id}"):
- target_entry_hash = entry.get("entry_hash")
target_entry = entry
break
- if target_entry_hash:
+ if target_entry:
break
- if not target_entry_hash:
+ if not target_entry:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail=f"Pending entry {entry_id} not found in Beancount ledger"
)
- # 3. Get the entry context (source text + sha256sum)
- context = await fava.get_entry_context(target_entry_hash)
- source = context.get("slice", "")
- sha256sum = context.get("sha256sum", "")
-
- if not source:
- raise HTTPException(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- detail="Could not retrieve entry source from Fava"
- )
-
- # 4. Change flag from ! to *
- # Replace the first occurrence of the date + ! pattern
- import re
+ # Get entry metadata for file location
+ meta = target_entry.get("meta", {})
+ filename = meta.get("filename")
+ lineno = meta.get("lineno")
date_str = target_entry.get("date", "")
- old_pattern = f"{date_str} !"
- new_pattern = f"{date_str} *"
- if old_pattern not in source:
+ if not filename or not lineno:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- detail=f"Could not find pending flag pattern '{old_pattern}' in entry source"
+ detail="Entry metadata missing filename or lineno"
)
- new_source = source.replace(old_pattern, new_pattern, 1)
+ # 3. Get the source file from Fava
+ async with httpx.AsyncClient(timeout=fava.timeout) as client:
+ response = await client.get(
+ f"{fava.base_url}/source",
+ params={"filename": filename}
+ )
+ response.raise_for_status()
+ source_data = response.json()["data"]
- # 5. Update the entry via Fava API
- await fava.update_entry_source(target_entry_hash, new_source, sha256sum)
+ sha256sum = source_data["sha256sum"]
+ source = source_data["source"]
+ lines = source.split('\n')
+
+ # 4. Find and modify the entry at the specified line
+ # Line numbers are 1-indexed, list is 0-indexed
+ entry_line_idx = lineno - 1
+
+ if entry_line_idx >= len(lines):
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail=f"Line {lineno} not found in source file"
+ )
+
+ entry_line = lines[entry_line_idx]
+
+ # Check if the line contains the pending flag pattern
+ old_pattern = f"{date_str} !"
+ if old_pattern not in entry_line:
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail=f"Line {lineno} does not contain expected pattern '{old_pattern}'. Found: {entry_line}"
+ )
+
+ # Replace the flag
+ new_pattern = f"{date_str} *"
+ new_line = entry_line.replace(old_pattern, new_pattern, 1)
+ lines[entry_line_idx] = new_line
+
+ # 5. Write back the modified source
+ new_source = '\n'.join(lines)
+
+ update_response = await client.put(
+ f"{fava.base_url}/source",
+ json={
+ "file_path": filename,
+ "source": new_source,
+ "sha256sum": sha256sum
+ },
+ headers={"Content-Type": "application/json"}
+ )
+ update_response.raise_for_status()
+
+ logger.info(f"Entry {entry_id} approved (flag changed to *)")
return {
"message": f"Entry {entry_id} approved successfully",
"entry_id": entry_id,
- "entry_hash": target_entry_hash,
"date": date_str,
"description": target_entry.get("narration", "")
}
@@ -2725,30 +2758,23 @@ 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
+ import httpx
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
all_entries = await fava.get_journal_entries()
# 2. Find the entry with matching castle ID in links
- target_entry_hash = None
target_entry = None
for entry in all_entries:
@@ -2760,58 +2786,77 @@ async def api_reject_expense_entry(
link_clean = link.lstrip('^')
# Check if this entry has our castle ID
if link_clean == f"castle-{entry_id}" or link_clean.endswith(f"-{entry_id}"):
- target_entry_hash = entry.get("entry_hash")
target_entry = entry
break
- if target_entry_hash:
+ if target_entry:
break
- if not target_entry_hash:
+ if not target_entry:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail=f"Pending entry {entry_id} not found in Beancount ledger"
)
- # 3. Get the entry context (source text + sha256sum)
- context = await fava.get_entry_context(target_entry_hash)
- source = context.get("slice", "")
- sha256sum = context.get("sha256sum", "")
-
- if not source:
- raise HTTPException(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- detail="Could not retrieve entry source from Fava"
- )
-
- # 4. Add #voided tag (keep ! flag as per convention)
+ # Get entry metadata for file location
+ meta = target_entry.get("meta", {})
+ filename = meta.get("filename")
+ lineno = meta.get("lineno")
date_str = target_entry.get("date", "")
- # Add #voided tag if not already present
- if "#voided" not in source:
- # Find the transaction line and add #voided to the tags
- # Pattern: date ! "narration" #existing-tags
- lines = source.split('\n')
- for i, line in enumerate(lines):
- if date_str in line and '"' in line and '!' in line:
- # Add #voided tag to the transaction line
- if '#' in line:
- # Already has tags, append voided
- lines[i] = line.rstrip() + ' #voided'
- else:
- # No tags yet, add after narration
- lines[i] = line.rstrip() + ' #voided'
- break
- new_source = '\n'.join(lines)
- else:
- new_source = source
+ if not filename or not lineno:
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Entry metadata missing filename or lineno"
+ )
- # 5. Update the entry via Fava API
- await fava.update_entry_source(target_entry_hash, new_source, sha256sum)
+ # 3. Get the source file from Fava
+ async with httpx.AsyncClient(timeout=fava.timeout) as client:
+ response = await client.get(
+ f"{fava.base_url}/source",
+ params={"filename": filename}
+ )
+ response.raise_for_status()
+ source_data = response.json()["data"]
+
+ sha256sum = source_data["sha256sum"]
+ source = source_data["source"]
+ lines = source.split('\n')
+
+ # 4. Find and modify the entry at the specified line - add #voided tag
+ entry_line_idx = lineno - 1
+
+ if entry_line_idx >= len(lines):
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail=f"Line {lineno} not found in source file"
+ )
+
+ entry_line = lines[entry_line_idx]
+
+ # Add #voided tag if not already present
+ if "#voided" not in entry_line:
+ # Add #voided tag to the transaction line
+ new_line = entry_line.rstrip() + ' #voided'
+ lines[entry_line_idx] = new_line
+
+ # 5. Write back the modified source
+ new_source = '\n'.join(lines)
+
+ update_response = await client.put(
+ f"{fava.base_url}/source",
+ json={
+ "file_path": filename,
+ "source": new_source,
+ "sha256sum": sha256sum
+ },
+ headers={"Content-Type": "application/json"}
+ )
+ update_response.raise_for_status()
+ logger.info(f"Entry {entry_id} rejected (added #voided tag)")
return {
"message": f"Entry {entry_id} rejected (marked as voided)",
"entry_id": entry_id,
- "entry_hash": target_entry_hash,
"date": date_str,
"description": target_entry.get("narration", "")
}
@@ -2823,10 +2868,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 +2880,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 +2914,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 +2949,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 +2974,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 +2990,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 +3007,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 +3028,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 +3079,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 +3110,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 +3140,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