diff --git a/routstr/core/admin.py b/routstr/core/admin.py index f84fc8b9..fa2381b1 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -169,10 +169,22 @@ class SettingsUpdate(BaseModel): __root__: dict[str, object] +class PasswordUpdate(BaseModel): + current_password: str + new_password: str + + @admin_router.patch("/api/settings", dependencies=[Depends(require_admin_api)]) async def update_settings(request: Request, update: SettingsUpdate) -> dict: + # Remove sensitive fields from general settings update + settings_data = update.__root__.copy() + sensitive_fields = ["admin_password", "upstream_api_key", "nsec"] + for field in sensitive_fields: + if field in settings_data: + del settings_data[field] + async with create_session() as session: - new_settings = await SettingsService.update(update.__root__, session) + new_settings = await SettingsService.update(settings_data, session) data = new_settings.dict() if "upstream_api_key" in data: data["upstream_api_key"] = "[REDACTED]" if data["upstream_api_key"] else "" @@ -183,6 +195,35 @@ async def update_settings(request: Request, update: SettingsUpdate) -> dict: return data +@admin_router.patch("/api/password", dependencies=[Depends(require_admin_api)]) +async def update_password(request: Request, password_update: PasswordUpdate) -> dict: + # Verify current password + try: + current_settings = SettingsService.get() + current_password = current_settings.admin_password + except Exception: + current_password = os.getenv("ADMIN_PASSWORD", "") + + if not current_password: + raise HTTPException(status_code=500, detail="Admin password not configured") + + if password_update.current_password != current_password: + raise HTTPException(status_code=401, detail="Current password is incorrect") + + # Validate new password + new_password = password_update.new_password.strip() + if len(new_password) < 6: + raise HTTPException( + status_code=400, detail="New password must be at least 6 characters" + ) + + # Update password + async with create_session() as session: + await SettingsService.update({"admin_password": new_password}, session) + + return {"ok": True, "message": "Password updated successfully"} + + class SetupRequest(BaseModel): password: str diff --git a/ui/app/favicon.ico b/ui/app/favicon.ico index 718d6fea..c4591ac4 100644 Binary files a/ui/app/favicon.ico and b/ui/app/favicon.ico differ diff --git a/ui/components/settings/admin-settings.tsx b/ui/components/settings/admin-settings.tsx index b56f45ed..256ddb24 100644 --- a/ui/components/settings/admin-settings.tsx +++ b/ui/components/settings/admin-settings.tsx @@ -24,7 +24,6 @@ interface SettingsData { description?: string; npub?: string; nsec?: string; - admin_password?: string; upstream_api_key?: string; http_url?: string; onion_url?: string; @@ -32,6 +31,12 @@ interface SettingsData { [key: string]: unknown; } +interface PasswordData { + current_password: string; + new_password: string; + confirm_password: string; +} + export function AdminSettings() { const [settings, setSettings] = useState({}); const [loading, setLoading] = useState(true); @@ -39,6 +44,13 @@ export function AdminSettings() { const [error, setError] = useState(''); const [showSecrets, setShowSecrets] = useState(false); const [newMint, setNewMint] = useState(''); + const [passwordData, setPasswordData] = useState({ + current_password: '', + new_password: '', + confirm_password: '', + }); + const [passwordError, setPasswordError] = useState(''); + const [passwordSaving, setPasswordSaving] = useState(false); useEffect(() => { loadSettings(); @@ -78,6 +90,45 @@ export function AdminSettings() { } }; + const handlePasswordUpdate = async () => { + try { + setPasswordSaving(true); + setPasswordError(''); + + if (passwordData.new_password !== passwordData.confirm_password) { + setPasswordError('New passwords do not match'); + return; + } + + if (passwordData.new_password.length < 6) { + setPasswordError('New password must be at least 6 characters'); + return; + } + + const { apiClient } = await import('@/lib/api/client'); + + await apiClient.patch('/admin/api/password', { + current_password: passwordData.current_password, + new_password: passwordData.new_password, + }); + + setPasswordData({ + current_password: '', + new_password: '', + confirm_password: '', + }); + + toast.success('Password updated successfully'); + } catch (err) { + const message = + err instanceof Error ? err.message : 'Failed to update password'; + setPasswordError(message); + toast.error(message); + } finally { + setPasswordSaving(false); + } + }; + const handleInputChange = (field: string, value: string | boolean) => { setSettings((prev) => ({ ...prev, @@ -246,11 +297,6 @@ export function AdminSettings() { - {renderSecretField( - 'admin_password', - 'Admin Password', - 'Enter admin password' - )} {renderSecretField( 'upstream_api_key', 'Upstream API Key', @@ -258,7 +304,6 @@ export function AdminSettings() { )} - {/* Cashu Mints */} @@ -307,24 +352,104 @@ export function AdminSettings() { )} - - - - - - - + + + + + + + + {/* Password Change */} + + + Change Admin Password + + Update your admin password for enhanced security + + + + {passwordError && ( + + + {passwordError} + + )} + +
+ + + setPasswordData((prev) => ({ + ...prev, + current_password: e.target.value, + })) + } + placeholder='Enter current password' + /> +
+ +
+ + + setPasswordData((prev) => ({ + ...prev, + new_password: e.target.value, + })) + } + placeholder='Enter new password (min 6 characters)' + /> +
+ +
+ + + setPasswordData((prev) => ({ + ...prev, + confirm_password: e.target.value, + })) + } + placeholder='Confirm new password' + /> +
+
+ + + +
+ ); }