mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
* fix create_order issue * fix url and better error-handling * basic balances tab * activating services * Service management * Improve settings page and default to it if token unset * Fix history tab * Fix trade error handling * Add withdraw * calling specter-cloud for creating vaultoro orders * Improve trade screen and fixes * sidebar fix * black * refactor Service integration * refactor to have Service Classes like manifests * maturity * dynamic initialisation of service-classes and blueprints * fix sidebar_services * migrated templates and static into vaultoro folder * refactor config to manifest * some minor things * swan initial * rename and fix test * adding ServiceApiKeyStorageUserAware * fix * store the access token * directory indirection to shield templates from each others blueprint * first attempts with automatic withdrawals * proper tab highlighting * Update service_apikey_storage.py * Update oauth2_success.jinja * Awaiting refresh_token support * Service logo display on Addresses * Associate addr with a Service * address-data component reorg Separates the presentation html from the data as much as possible. * Services data/icon added to tx History * Now hitting the updated Swan endpoint to save deposit addrs * Simplified injecting Services data into JS * Reducing js calls back to server in tx-data; templatizing utxo in/outs * renaming "reserving" to "associating" an Address with a Service. * rename `manifest.py` files to `service.py`. * rename "api_data" to "service_data" to make the storage a bit more generalized. `ServiceApiKeyStorage` is now `ServiceEncryptedStorage` to match. * Beginning of factoring out Swan api to its own `api.py` file; need to rectify with `swan_client.py`. * Removed tx-table/row/data changes and address-table/row/data Kept only the bare minimum changes required to display the Services icon, plus optimizations. * deleted no longer used CustomElement * Adding services docs to mkdocs * Configuration for Services * more clever configuration * fix test * deleted swan_client * Changing the address abbreviation format to 7...7 and little big fix for not vertically aligned addresses in Firefox. * Better state management if Auth method changes * Cleanup, better user messaging; pulling Service methods out of controller and User * Redirect to services endpoint after setting up authentication. * PR cleanup, bug fixes, test suite updates * Fixed test case problem * First fix for delete API key button. * Service hooks; Option to fully remove Swan Integration; Logout clears plaintext_user_secret * Restoring bugfix from @moneymanolis * cleanup and black * Make service-decovery in AppImage work * import hashlib, maybe fix cypress * Update service_encrypted_storage.py * further bugfix on update * TODO: remove debugging in client.py before first release * fix controller-test * fix singleton testing issues Co-authored-by: Kim Neunert <k9ert@gmx.de> Co-authored-by: benk10 <ben.kaufman10@gmail.com> Co-authored-by: Kim Neunert <kneunert@gmail.com> Co-authored-by: moneymanolis <moneymanolis@protonmail.com>
677 lines
28 KiB
Python
677 lines
28 KiB
Python
import cryptography
|
|
import json
|
|
import logging
|
|
import os
|
|
import platform
|
|
import random
|
|
import secrets
|
|
import shutil
|
|
import sys
|
|
import tarfile
|
|
import time
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import pgpy
|
|
import requests
|
|
from flask import Blueprint, Flask
|
|
from flask import current_app as app
|
|
from flask import flash, jsonify, redirect, render_template, request, send_file, url_for
|
|
from flask_babel import lazy_gettext as _
|
|
from flask_login import current_user, login_required
|
|
from cryptoadvance.specter.services.service import Service
|
|
|
|
from cryptoadvance.specter.user import User, UserSecretException
|
|
|
|
from ..helpers import (
|
|
get_loglevel,
|
|
get_startblock_by_chain,
|
|
notify_upgrade,
|
|
set_loglevel,
|
|
)
|
|
from ..persistence import write_devices, write_wallet
|
|
from ..specter_error import ExtProcTimeoutException, handle_exception
|
|
from ..util.sha256sum import sha256sum
|
|
from ..util.shell import get_last_lines_from_file
|
|
from ..util.tor import start_hidden_service, stop_hidden_services
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
rand = random.randint(0, 1e32) # to force style refresh
|
|
|
|
# Setup endpoint blueprint
|
|
settings_endpoint = Blueprint("settings_endpoint", __name__)
|
|
|
|
|
|
@settings_endpoint.route("/", methods=["GET"])
|
|
@login_required
|
|
def settings():
|
|
return redirect(url_for("settings_endpoint.general"))
|
|
|
|
|
|
@settings_endpoint.route("/general", methods=["GET", "POST"])
|
|
@login_required
|
|
def general():
|
|
current_version = notify_upgrade(app, flash)
|
|
explorer_id = app.specter.explorer_id
|
|
fee_estimator = app.specter.fee_estimator
|
|
fee_estimator_custom_url = app.specter.config.get("fee_estimator_custom_url", "")
|
|
loglevel = get_loglevel(app)
|
|
unit = app.specter.unit
|
|
services = app.specter.service_manager.services
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
|
|
autohide_sensitive_info_timeout = request.form[
|
|
"autohide_sensitive_info_timeout"
|
|
]
|
|
if autohide_sensitive_info_timeout == "NEVER":
|
|
autohide_sensitive_info_timeout = None
|
|
elif autohide_sensitive_info_timeout == "CUSTOM":
|
|
autohide_sensitive_info_timeout = int(
|
|
request.form["custom_autohide_sensitive_info_timeout"]
|
|
)
|
|
else:
|
|
autohide_sensitive_info_timeout = int(autohide_sensitive_info_timeout)
|
|
|
|
if "autologout_timeout" in request.form:
|
|
# Is only in the form if specter.config.auth.method != "none"
|
|
autologout_timeout = request.form["autologout_timeout"]
|
|
if autologout_timeout == "NEVER":
|
|
autologout_timeout = None
|
|
elif autologout_timeout == "CUSTOM":
|
|
autologout_timeout = int(request.form["custom_autologout_timeout"])
|
|
else:
|
|
autologout_timeout = int(autologout_timeout)
|
|
else:
|
|
autologout_timeout = None
|
|
|
|
explorer_id = request.form["explorer"]
|
|
explorer_data = app.config["EXPLORERS_LIST"][explorer_id]
|
|
if explorer_id == "CUSTOM":
|
|
explorer_data["url"] = request.form["custom_explorer"]
|
|
fee_estimator = request.form["fee_estimator"]
|
|
fee_estimator_custom_url = request.form["fee_estimator_custom_url"]
|
|
unit = request.form["unit"]
|
|
validate_merkleproof_bool = request.form.get("validatemerkleproof") == "on"
|
|
if current_user.is_admin:
|
|
active_services = []
|
|
for service_name in services:
|
|
if request.form.get(f"service_{service_name}"):
|
|
active_services.append(service_name)
|
|
|
|
loglevel = request.form["loglevel"]
|
|
|
|
if action == "save":
|
|
if current_user.is_admin:
|
|
set_loglevel(app, loglevel)
|
|
|
|
app.specter.config_manager.update_autohide_sensitive_info_timeout(
|
|
autohide_sensitive_info_timeout, current_user
|
|
)
|
|
app.specter.config_manager.update_autologout_timeout(
|
|
autologout_timeout, current_user
|
|
)
|
|
|
|
app.specter.update_explorer(explorer_id, explorer_data, current_user)
|
|
app.specter.update_unit(unit, current_user)
|
|
app.specter.update_merkleproof_settings(
|
|
validate_bool=validate_merkleproof_bool
|
|
)
|
|
app.specter.service_manager.set_active_services(active_services)
|
|
app.specter.update_fee_estimator(
|
|
fee_estimator=fee_estimator,
|
|
custom_url=fee_estimator_custom_url,
|
|
user=current_user,
|
|
)
|
|
app.specter.check()
|
|
|
|
elif action == "restore":
|
|
restore_devices = []
|
|
restore_wallets = []
|
|
if request.form.get("restoredevices", ""):
|
|
restore_devices = json.loads(request.form.get("restoredevices", "[]"))
|
|
if request.form.get("restorewallets", ""):
|
|
restore_wallets = json.loads(request.form.get("restorewallets", "[]"))
|
|
write_devices(restore_devices)
|
|
app.specter.device_manager.update()
|
|
|
|
rescanning = False
|
|
for wallet in restore_wallets:
|
|
try:
|
|
app.specter.wallet_manager.rpc.createwallet(
|
|
os.path.join(
|
|
app.specter.wallet_manager.rpc_path, wallet["alias"]
|
|
),
|
|
True,
|
|
)
|
|
except Exception as e:
|
|
# if wallet already exists in Bitcoin Core
|
|
# continue with the existing one
|
|
if "already exists" not in str(e):
|
|
flash(
|
|
_("Failed to import wallet {}, error: {}").format(
|
|
wallet["name"], e
|
|
),
|
|
"error",
|
|
)
|
|
handle_exception(e)
|
|
continue
|
|
logger.debug(
|
|
f"Wallet {wallet['alias']} already exists, skipping creation"
|
|
)
|
|
write_wallet(wallet)
|
|
app.specter.wallet_manager.update(use_threading=False)
|
|
|
|
try:
|
|
wallet_obj = app.specter.wallet_manager.get_by_alias(
|
|
wallet["alias"]
|
|
)
|
|
wallet_obj.keypoolrefill(0, wallet_obj.IMPORT_KEYPOOL, change=False)
|
|
wallet_obj.keypoolrefill(0, wallet_obj.IMPORT_KEYPOOL, change=True)
|
|
wallet_obj.import_labels(wallet.get("labels", {}))
|
|
try:
|
|
wallet_obj.rpc.rescanblockchain(
|
|
wallet["blockheight"]
|
|
if "blockheight" in wallet
|
|
else get_startblock_by_chain(app.specter),
|
|
no_wait=True,
|
|
)
|
|
app.logger.info("Rescanning Blockchain ...")
|
|
rescanning = True
|
|
except Exception as e:
|
|
app.logger.error(
|
|
"Exception while rescanning blockchain for wallet {}: {}".format(
|
|
wallet["alias"], e
|
|
)
|
|
)
|
|
flash(
|
|
_("Failed to perform rescan for wallet: {}").format(e),
|
|
"error",
|
|
)
|
|
wallet_obj.getdata()
|
|
except Exception as e:
|
|
flash(
|
|
_("Failed to import wallet {}").format(wallet["name"]), "error"
|
|
)
|
|
handle_exception(e)
|
|
flash(_("Specter data was successfully loaded from backup"), "info")
|
|
if rescanning:
|
|
flash(
|
|
_(
|
|
"Wallets are rescanning for transactions history.\n\
|
|
This may take a few hours to complete."
|
|
),
|
|
"info",
|
|
)
|
|
|
|
return render_template(
|
|
"settings/general_settings.jinja",
|
|
services=services,
|
|
fee_estimator=fee_estimator,
|
|
fee_estimator_custom_url=fee_estimator_custom_url,
|
|
loglevel=loglevel,
|
|
validate_merkle_proofs=app.specter.config.get("validate_merkle_proofs") is True,
|
|
unit=unit,
|
|
specter=app.specter,
|
|
current_version=current_version,
|
|
rand=rand,
|
|
supported_languages=app.supported_languages,
|
|
)
|
|
|
|
|
|
@settings_endpoint.route("/tor", methods=["GET", "POST"])
|
|
@login_required
|
|
def tor():
|
|
"""
|
|
controls the tor related settings
|
|
GET for displaying the page, POST for updates
|
|
param action might be "save", "test_tor" or "toggle_hidden_service"
|
|
param proxy_url the Tor deamon url, usually something like socks5h://localhost:9050
|
|
param only_tor "on" or something else ("off")
|
|
"""
|
|
if not current_user.is_admin:
|
|
flash(_("Only an admin is allowed to access this page."), "error")
|
|
return redirect("")
|
|
app.specter.reset_setup("torbrowser")
|
|
current_version = notify_upgrade(app, flash)
|
|
proxy_url = app.specter.proxy_url
|
|
only_tor = app.specter.only_tor
|
|
tor_control_port = app.specter.tor_control_port
|
|
tor_type = app.specter.tor_type
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
tor_type = request.form["tor_type"]
|
|
proxy_url = request.form["proxy_url"]
|
|
only_tor = request.form.get("only_tor") == "on"
|
|
tor_control_port = request.form["tor_control_port"]
|
|
hidden_service = request.form.get("hidden_service") == "on"
|
|
|
|
if action == "save":
|
|
logger.info("Updating Tor settings...")
|
|
app.specter.update_tor_type(tor_type, current_user)
|
|
|
|
if tor_type == "custom":
|
|
app.specter.update_proxy_url(proxy_url, current_user)
|
|
app.specter.update_tor_control_port(tor_control_port, current_user)
|
|
else:
|
|
proxy_url = "socks5h://localhost:9050"
|
|
tor_control_port = ""
|
|
|
|
app.specter.update_only_tor(only_tor, current_user)
|
|
if hidden_service != app.specter.config["tor_status"]:
|
|
if not app.config["DEBUG"]:
|
|
if app.specter.config["auth"].get("method", "none") == "none":
|
|
flash(
|
|
"Enabling Tor hidden service will expose your Specter for remote access.<br>It is therefore required that you set up authentication tab for Specter first to prevent unauthorized access.<br><br>Please go to Settings -> Authentication and set up an authentication method and retry.",
|
|
"error",
|
|
)
|
|
else:
|
|
if hasattr(current_user, "is_admin") and current_user.is_admin:
|
|
if not hidden_service:
|
|
stop_hidden_services(app)
|
|
app.specter.toggle_tor_status()
|
|
flash(
|
|
"Tor hidden service turn off successfully", "info"
|
|
)
|
|
else:
|
|
try:
|
|
start_hidden_service(app)
|
|
app.specter.toggle_tor_status()
|
|
flash(
|
|
"Tor hidden service turn on successfully",
|
|
"info",
|
|
)
|
|
except Exception as e:
|
|
handle_exception(e)
|
|
flash(
|
|
"Failed to start Tor hidden service. Make sure you have Tor running with ControlPort configured and try again. Error returned: {}".format(
|
|
e
|
|
),
|
|
"error",
|
|
)
|
|
else:
|
|
flash(
|
|
"Can't toggle hidden service while Specter is running in DEBUG mode",
|
|
"error",
|
|
)
|
|
|
|
app.specter.check()
|
|
|
|
elif action == "starttor":
|
|
logger.info("Starting Tor...")
|
|
try:
|
|
app.specter.tor_daemon.start_tor_daemon()
|
|
flash(_("Specter has started Tor"))
|
|
except Exception as e:
|
|
flash(_("Failed to start Tor, error: {}").format(e), "error")
|
|
logger.error(f"Failed to start Tor, error: {e}")
|
|
elif action == "stoptor":
|
|
logger.info("Stopping Tor...")
|
|
try:
|
|
app.specter.tor_daemon.stop_tor_daemon()
|
|
time.sleep(1)
|
|
flash(_("Specter stopped Tor successfully"))
|
|
except Exception as e:
|
|
flash(_("Failed to stop Tor, error: {}").format(e), "error")
|
|
logger.error(f"Failed to start Tor, error: {e}")
|
|
elif action == "uninstalltor":
|
|
logger.info("Uninstalling Tor...")
|
|
try:
|
|
if app.specter.is_tor_dameon_running():
|
|
app.specter.tor_daemon.stop_tor_daemon()
|
|
shutil.rmtree(os.path.join(app.specter.data_folder, "tor-binaries"))
|
|
os.remove(os.path.join(app.specter.data_folder, "torrc"))
|
|
flash(_("Tor uninstalled successfully"))
|
|
except Exception as e:
|
|
flash(_("Failed to uninstall Tor, error: {}").format(e), "error")
|
|
logger.error(f"Failed to uninstall Tor, error: {e}")
|
|
elif action == "test_tor":
|
|
logger.info("Testing the Tor connection...")
|
|
try:
|
|
requests_session = requests.Session()
|
|
requests_session.proxies["http"] = proxy_url
|
|
requests_session.proxies["https"] = proxy_url
|
|
res = requests_session.get(
|
|
"http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion/", # Tor Project onion v3 website
|
|
timeout=30,
|
|
)
|
|
tor_connectable = res.status_code == 200
|
|
if tor_connectable:
|
|
flash(_("Tor requests test completed successfully!"), "info")
|
|
else:
|
|
flash(
|
|
_(
|
|
"Failed to make test request over Tor. Status-Code: {}"
|
|
).format(res.status_code),
|
|
"error",
|
|
)
|
|
logger.error(
|
|
f"Failed to make test request over Tor. Status-Code: {res.status_code}"
|
|
)
|
|
if tor_type == "builtin":
|
|
logger.error("Tor-Logs:")
|
|
app.specter.tor_daemon.stop_tor_daemon()
|
|
time.sleep(1)
|
|
logger.error(app.specter.tor_daemon.get_logs())
|
|
app.specter.tor_daemon.start_tor_daemon()
|
|
except Exception as e:
|
|
flash(
|
|
_("Failed to make test request over Tor.\nError: {}").format(e),
|
|
"error",
|
|
)
|
|
logger.error(f"Failed to make test request over Tor.\nError: {e}")
|
|
if tor_type == "builtin":
|
|
logger.error("Tor-Logs:")
|
|
app.specter.tor_daemon.stop_tor_daemon()
|
|
time.sleep(1)
|
|
logger.error(app.specter.tor_daemon.get_logs())
|
|
app.specter.tor_daemon.start_tor_daemon()
|
|
tor_connectable = False
|
|
|
|
return render_template(
|
|
"settings/tor_settings.jinja",
|
|
tor_type=tor_type,
|
|
proxy_url=proxy_url,
|
|
only_tor=only_tor,
|
|
tor_control_port=tor_control_port,
|
|
tor_service_id=app.tor_service_id,
|
|
torbrowser_installed=os.path.isfile(app.specter.torbrowser_path),
|
|
torbrowser_running=app.specter.is_tor_dameon_running(),
|
|
specter=app.specter,
|
|
current_version=current_version,
|
|
rand=rand,
|
|
)
|
|
|
|
|
|
@settings_endpoint.route("/auth", methods=["GET", "POST"])
|
|
@login_required
|
|
def auth():
|
|
# TODO: Simplify this endpoint. Separate out setting the Authentication mode from all
|
|
# the other options here: updating admin username/password, adding users, deleting
|
|
# users, etc. Do those in simple separate screens with their own endpoints.
|
|
current_version = notify_upgrade(app, flash)
|
|
auth = app.specter.config["auth"]
|
|
method = auth["method"]
|
|
rate_limit = auth["rate_limit"]
|
|
registration_link_timeout = auth["registration_link_timeout"]
|
|
users = None
|
|
if current_user.is_admin and method == "usernamepassword":
|
|
users = [user for user in app.specter.user_manager.users if not user.is_admin]
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
|
|
if action == "save":
|
|
if "specter_username" in request.form:
|
|
specter_username = request.form["specter_username"]
|
|
specter_password = request.form["specter_password"]
|
|
else:
|
|
specter_username = None
|
|
specter_password = None
|
|
|
|
if current_user.is_admin:
|
|
method = request.form["method"]
|
|
rate_limit = request.form["rate_limit"]
|
|
registration_link_timeout = request.form["registration_link_timeout"]
|
|
|
|
min_chars = int(auth["password_min_chars"])
|
|
if specter_username:
|
|
|
|
if current_user.username != specter_username:
|
|
if app.specter.user_manager.get_user_by_username(specter_username):
|
|
flash(
|
|
_("Username is already taken, please choose another one"),
|
|
"error",
|
|
)
|
|
return render_template(
|
|
"settings/auth_settings.jinja",
|
|
method=method,
|
|
rate_limit=rate_limit,
|
|
registration_link_timeout=registration_link_timeout,
|
|
users=users,
|
|
specter=app.specter,
|
|
current_version=current_version,
|
|
rand=rand,
|
|
has_service_encrypted_storage=app.specter.service_manager.user_has_encrypted_storage(
|
|
current_user
|
|
),
|
|
)
|
|
current_user.username = specter_username
|
|
|
|
if specter_password:
|
|
if len(specter_password) < min_chars:
|
|
flash(
|
|
_(
|
|
"Please enter a password of a least {} characters"
|
|
).format(min_chars),
|
|
"error",
|
|
)
|
|
return render_template(
|
|
"settings/auth_settings.jinja",
|
|
method=method,
|
|
rate_limit=rate_limit,
|
|
registration_link_timeout=registration_link_timeout,
|
|
users=users,
|
|
specter=app.specter,
|
|
current_version=current_version,
|
|
rand=rand,
|
|
has_service_encrypted_storage=app.specter.service_manager.user_has_encrypted_storage(
|
|
current_user
|
|
),
|
|
)
|
|
current_user.set_password(specter_password)
|
|
|
|
current_user.save_info()
|
|
|
|
if current_user.is_admin:
|
|
app.specter.update_auth(method, rate_limit, registration_link_timeout)
|
|
if method in ["rpcpasswordaspin", "passwordonly", "usernamepassword"]:
|
|
if method == "passwordonly":
|
|
specter_password = request.form.get("specter_password_only")
|
|
if specter_password and len(specter_password) < min_chars:
|
|
flash(
|
|
_(
|
|
"Please enter a password of a least {} characters"
|
|
).format(min_chars),
|
|
"error",
|
|
)
|
|
return render_template(
|
|
"settings/auth_settings.jinja",
|
|
method=method,
|
|
rate_limit=rate_limit,
|
|
registration_link_timeout=registration_link_timeout,
|
|
users=users,
|
|
specter=app.specter,
|
|
current_version=current_version,
|
|
rand=rand,
|
|
has_service_encrypted_storage=app.specter.service_manager.user_has_encrypted_storage(
|
|
current_user
|
|
),
|
|
)
|
|
elif not specter_password:
|
|
# Set to the default
|
|
specter_password = "admin"
|
|
|
|
try:
|
|
current_user.set_password(specter_password)
|
|
except UserSecretException as e:
|
|
# Most likely the admin User is logged in but the
|
|
# plaintext_user_secret is not available in memory (happens
|
|
# when the server restarts).
|
|
logger.warn(e)
|
|
flash(
|
|
_(
|
|
"Error re-encrypting Service data. Log out and log back in before trying again."
|
|
),
|
|
"error",
|
|
)
|
|
return render_template(
|
|
"settings/auth_settings.jinja",
|
|
method=method,
|
|
rate_limit=rate_limit,
|
|
registration_link_timeout=registration_link_timeout,
|
|
users=users,
|
|
specter=app.specter,
|
|
current_version=current_version,
|
|
rand=rand,
|
|
has_service_encrypted_storage=app.specter.service_manager.user_has_encrypted_storage(
|
|
current_user
|
|
),
|
|
)
|
|
|
|
current_user.save_info()
|
|
|
|
flash(
|
|
_("Admin password successfully updated"),
|
|
"info",
|
|
)
|
|
|
|
if method == "usernamepassword":
|
|
users = [
|
|
user
|
|
for user in app.specter.user_manager.users
|
|
if not user.is_admin
|
|
]
|
|
else:
|
|
users = None
|
|
|
|
app.config["LOGIN_DISABLED"] = False
|
|
else:
|
|
users = None
|
|
app.config["LOGIN_DISABLED"] = True
|
|
|
|
# Cannot support Services if there's no password (admin was already
|
|
# warned about this in the UI). Remove User.services, clear the
|
|
# `user_secret`, and wipe the ServiceEncryptedStorage.
|
|
app.specter.service_manager.remove_all_services_from_user(
|
|
current_user
|
|
)
|
|
|
|
# Redirect if a URL was given via the next variable
|
|
if request.form.get("next") and request.form.get("next") != "":
|
|
return redirect(request.form.get("next"))
|
|
app.specter.check()
|
|
|
|
elif action == "adduser":
|
|
if current_user.is_admin:
|
|
new_otp = secrets.token_urlsafe(16)
|
|
now = time.time()
|
|
timeout = int(registration_link_timeout)
|
|
timeout = 0 if timeout < 0 else timeout
|
|
if timeout > 0:
|
|
expiry = now + timeout * 60 * 60
|
|
if timeout > 1:
|
|
expiry_desc = " " + _("(expires in {} hours)").format(timeout)
|
|
else:
|
|
expiry_desc = " " + _("(expires in 1 hour)")
|
|
else:
|
|
expiry = 0
|
|
expiry_desc = ""
|
|
app.specter.otp_manager.add_new_user_otp(
|
|
{"otp": new_otp, "created_at": now, "expiry": expiry}
|
|
)
|
|
flash(
|
|
_("New user link generated{}: {}auth/register?otp={}").format(
|
|
expiry_desc, request.url_root, new_otp
|
|
),
|
|
"info",
|
|
)
|
|
else:
|
|
flash(
|
|
_("Error: Only the admin account can issue new registration links"),
|
|
"error",
|
|
)
|
|
|
|
elif action == "deleteuser":
|
|
delete_user = request.form["deleteuser"]
|
|
user = app.specter.user_manager.get_user(delete_user)
|
|
if current_user.is_admin:
|
|
# TODO: delete should be done by UserManager
|
|
app.specter.delete_user(user)
|
|
users.remove(user)
|
|
flash(
|
|
_("User {} was deleted successfully").format(user.username), "info"
|
|
)
|
|
else:
|
|
flash(_("Error: Only the admin account can delete users"), "error")
|
|
|
|
return render_template(
|
|
"settings/auth_settings.jinja",
|
|
method=method,
|
|
rate_limit=rate_limit,
|
|
registration_link_timeout=registration_link_timeout,
|
|
users=users,
|
|
specter=app.specter,
|
|
current_version=current_version,
|
|
rand=rand,
|
|
has_service_encrypted_storage=app.specter.service_manager.user_has_encrypted_storage(
|
|
current_user
|
|
),
|
|
next=request.args.get("next", ""),
|
|
)
|
|
|
|
|
|
@settings_endpoint.route("/hwi", methods=["GET", "POST"])
|
|
@login_required
|
|
def hwi():
|
|
current_version = notify_upgrade(app, flash)
|
|
if request.method == "POST":
|
|
hwi_bridge_url = request.form["hwi_bridge_url"]
|
|
app.specter.update_hwi_bridge_url(hwi_bridge_url, current_user)
|
|
flash(_("HWIBridge URL is updated! Don't forget to whitelist Specter!"))
|
|
return render_template(
|
|
"settings/hwi_settings.jinja",
|
|
specter=app.specter,
|
|
current_version=current_version,
|
|
rand=rand,
|
|
)
|
|
|
|
|
|
@settings_endpoint.route("/assets/set_label", methods=["POST"])
|
|
@login_required
|
|
def set_asset_label():
|
|
asset = request.form["asset"]
|
|
label = request.form["label"].rstrip()
|
|
if label.lower() in ["btc", "bitcoin", "sat", "lbtc", "tlbtc"]:
|
|
return f'Label "{label}" is not allowed', 500
|
|
try:
|
|
app.specter.update_asset_label(asset, label)
|
|
except Exception as e:
|
|
return str(e), 500
|
|
return {"success": True}
|
|
|
|
|
|
@settings_endpoint.route("/assets/", methods=["GET"])
|
|
@login_required
|
|
def assets():
|
|
"""List of all known / labeled assets"""
|
|
return app.specter.asset_labels
|
|
|
|
|
|
@settings_endpoint.route("/assets/<asset>/", methods=["GET"])
|
|
@login_required
|
|
def get_asset(asset):
|
|
"""Get info about particular asset. Accepts either label or hex asset"""
|
|
try:
|
|
# check if we've got label, not an asset
|
|
if len(asset) != 64:
|
|
for a, lbl in app.specter.asset_labels.items():
|
|
if lbl == asset:
|
|
return {"asset": a, "label": lbl}
|
|
return {"error": "asset lookup by label failed"}, 404
|
|
return {"asset": asset, "label": app.specter.asset_label(asset)}
|
|
except Exception as e:
|
|
return {"error": str(e)}, 500
|
|
|
|
|
|
################## Settings util endpoints #######################
|
|
|
|
# Specter backup file
|
|
@settings_endpoint.route("/specter_backup.zip")
|
|
@login_required
|
|
def backup_file():
|
|
return send_file(
|
|
app.specter.specter_backup_file(),
|
|
attachment_filename="specter-backup.zip",
|
|
as_attachment=True,
|
|
)
|