From 315bcae4cacb0a3557ea662ca7f3629ed17efb6d Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 26 Jun 2025 14:07:22 +0200 Subject: [PATCH] Add client registration functionality: Implement API endpoint for client self-registration, including validation and error handling. Update frontend to support registration form and status checks, enhancing user experience for DCA clients. --- crud.py | 70 ++++++++++++++- models.py | 7 ++ static/js/index.js | 98 ++++++++++++++++++-- templates/satmachineclient/index.html | 123 ++++++++++++++++++++++++-- views_api.py | 52 +++++++++++ 5 files changed, 338 insertions(+), 12 deletions(-) diff --git a/crud.py b/crud.py index 925451a..982594a 100644 --- a/crud.py +++ b/crud.py @@ -12,6 +12,7 @@ from .models import ( ClientTransaction, ClientAnalytics, UpdateClientSettings, + ClientRegistrationData, ) # Connect to admin extension's database @@ -446,4 +447,71 @@ async def update_client_dca_settings(client_id: str, settings: UpdateClientSetti ) return True except Exception: - return False \ No newline at end of file + return False + + +################################################### +############## CLIENT REGISTRATION ############### +################################################### + +async def register_dca_client(user_id: str, wallet_id: str, registration_data: ClientRegistrationData) -> Optional[dict]: + """Register a new DCA client - special permission for self-registration""" + from lnbits.helpers import urlsafe_short_hash + from lnbits.core.crud import get_user + + try: + # Verify user exists and get username + user = await get_user(user_id) + username = registration_data.username or (user.username if user else f"user_{user_id[:8]}") + + # Check if client already exists + existing_client = await db.fetchone( + "SELECT id FROM satoshimachine.dca_clients WHERE user_id = :user_id", + {"user_id": user_id} + ) + + if existing_client: + return {"error": "Client already registered", "client_id": existing_client[0]} + + # Create new client + client_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO satoshimachine.dca_clients + (id, user_id, wallet_id, username, dca_mode, fixed_mode_daily_limit, status, created_at, updated_at) + VALUES (:id, :user_id, :wallet_id, :username, :dca_mode, :fixed_mode_daily_limit, :status, :created_at, :updated_at) + """, + { + "id": client_id, + "user_id": user_id, + "wallet_id": wallet_id, + "username": username, + "dca_mode": registration_data.dca_mode, + "fixed_mode_daily_limit": registration_data.fixed_mode_daily_limit, + "status": "active", + "created_at": datetime.now(), + "updated_at": datetime.now() + } + ) + + return { + "success": True, + "client_id": client_id, + "message": f"DCA client registered successfully with {registration_data.dca_mode} mode" + } + + except Exception as e: + print(f"Error registering DCA client: {e}") + return {"error": f"Registration failed: {str(e)}"} + + +async def get_client_by_user_id(user_id: str) -> Optional[dict]: + """Get client by user_id - returns dict instead of model for easier access""" + try: + client = await db.fetchone( + "SELECT * FROM satoshimachine.dca_clients WHERE user_id = :user_id", + {"user_id": user_id} + ) + return dict(client) if client else None + except Exception: + return None \ No newline at end of file diff --git a/models.py b/models.py index bfc0e1f..c0a436f 100644 --- a/models.py +++ b/models.py @@ -61,3 +61,10 @@ class UpdateClientSettings(BaseModel): status: Optional[str] = None # 'active' or 'inactive' +class ClientRegistrationData(BaseModel): + """Data for client self-registration""" + dca_mode: str = "flow" # Default to flow mode + fixed_mode_daily_limit: Optional[int] = None + username: Optional[str] = None + + diff --git a/static/js/index.js b/static/js/index.js index 192256a..c6e6209 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -4,6 +4,18 @@ window.app = Vue.createApp({ delimiters: ['${', '}'], data: function () { return { + // Registration state + isRegistered: false, + registrationChecked: false, + showRegistrationDialog: false, + registrationForm: { + selectedWallet: null, + dca_mode: 'flow', + fixed_mode_daily_limit: null, + username: '' + }, + + // Dashboard state dashboardData: null, transactions: [], loading: true, @@ -60,6 +72,73 @@ window.app = Vue.createApp({ }, methods: { + // Registration Methods + async checkRegistrationStatus() { + try { + const { data } = await LNbits.api.request( + 'GET', + '/satmachineclient/api/v1/registration-status', + this.g.user.wallets[0].adminkey + ) + + this.isRegistered = data.is_registered + this.registrationChecked = true + + if (!this.isRegistered) { + this.showRegistrationDialog = true + // Pre-fill username and default wallet if available + this.registrationForm.username = this.g.user.username || '' + this.registrationForm.selectedWallet = this.g.user.wallets[0] || null + } + + return data + } catch (error) { + console.error('Error checking registration status:', error) + this.error = 'Failed to check registration status' + this.registrationChecked = true + } + }, + + async registerClient() { + try { + // Prepare registration data similar to the admin test client creation + const registrationData = { + dca_mode: this.registrationForm.dca_mode, + fixed_mode_daily_limit: this.registrationForm.fixed_mode_daily_limit, + username: this.registrationForm.username || this.g.user.username || `user_${this.g.user.id.substring(0, 8)}` + } + + const { data } = await LNbits.api.request( + 'POST', + '/satmachineclient/api/v1/register', + this.registrationForm.selectedWallet.adminkey, + registrationData + ) + + this.isRegistered = true + this.showRegistrationDialog = false + + this.$q.notify({ + type: 'positive', + message: data.message || 'Successfully registered for DCA!', + icon: 'check_circle', + position: 'top' + }) + + // Load dashboard data after successful registration + await this.loadDashboardData() + + } catch (error) { + console.error('Error registering client:', error) + this.$q.notify({ + type: 'negative', + message: error.detail || 'Failed to register for DCA', + position: 'top' + }) + } + }, + + // Dashboard Methods formatCurrency(amount) { if (!amount) return 'Q 0.00'; // Values are already in full currency units, not centavos @@ -662,11 +741,18 @@ window.app = Vue.createApp({ async created() { try { this.loading = true - await Promise.all([ - this.loadDashboardData(), - this.loadTransactions(), - this.loadChartData() - ]) + + // Check registration status first + await this.checkRegistrationStatus() + + // Only load dashboard data if registered + if (this.isRegistered) { + await Promise.all([ + this.loadDashboardData(), + this.loadTransactions(), + this.loadChartData() + ]) + } } catch (error) { console.error('Error initializing dashboard:', error) this.error = 'Failed to initialize dashboard' @@ -694,7 +780,7 @@ window.app = Vue.createApp({ computed: { hasData() { - return this.dashboardData && !this.loading + return this.dashboardData && !this.loading && this.isRegistered } }, diff --git a/templates/satmachineclient/index.html b/templates/satmachineclient/index.html index fd33b93..af2fa20 100644 --- a/templates/satmachineclient/index.html +++ b/templates/satmachineclient/index.html @@ -11,7 +11,7 @@
- +
Loading your DCA dashboard...
@@ -26,8 +26,19 @@
+ + + + +
Welcome to Bitcoin DCA!
+
+ Please complete your registration to start your Dollar Cost Averaging journey. +
+
+
+ -
+
@@ -233,8 +244,8 @@
- - + +
Bitcoin Accumulation Progress
@@ -262,7 +273,7 @@ -
+
@@ -511,5 +522,107 @@
+ + + + +
+ +
Welcome to DCA!
+
Let's set up your Bitcoin Dollar Cost Averaging account
+
+ + + + + + + + + + + + + + + + +
+ Flow Mode: Your Bitcoin purchases are proportional to your deposit balance.
+ Fixed Mode: Set a daily limit for consistent Bitcoin accumulation. +
+
+ +
+ + + Start My DCA Journey + +
+
+
+
{% endblock %} diff --git a/views_api.py b/views_api.py index 43b6b2f..12d4f08 100644 --- a/views_api.py +++ b/views_api.py @@ -15,17 +15,69 @@ from .crud import ( get_client_analytics, update_client_dca_settings, get_client_by_user_id, + register_dca_client, ) from .models import ( ClientDashboardSummary, ClientTransaction, ClientAnalytics, UpdateClientSettings, + ClientRegistrationData, ) satmachineclient_api_router = APIRouter() +################################################### +############## CLIENT REGISTRATION ############### +################################################### + +@satmachineclient_api_router.post("/api/v1/register", status_code=HTTPStatus.CREATED) +async def api_register_client( + registration_data: ClientRegistrationData, + wallet: WalletTypeInfo = Depends(require_admin_key), +) -> dict: + """Register a new DCA client + + Clients can self-register using their wallet admin key. + Creates a new client entry in the satoshimachine database. + """ + result = await register_dca_client( + wallet.wallet.user, + wallet.wallet.id, + registration_data + ) + + if "error" in result: + if "already registered" in result["error"]: + raise HTTPException( + status_code=HTTPStatus.CONFLICT, + detail=result["error"] + ) + else: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=result["error"] + ) + + return result + + +@satmachineclient_api_router.get("/api/v1/registration-status") +async def api_check_registration_status( + wallet: WalletTypeInfo = Depends(require_admin_key), +) -> dict: + """Check if user is already registered as a DCA client""" + client = await get_client_by_user_id(wallet.wallet.user) + + return { + "is_registered": client is not None, + "client_id": client["id"] if client else None, + "dca_mode": client["dca_mode"] if client else None, + "status": client["status"] if client else None, + } + + ################################################### ############## CLIENT DASHBOARD API ############### ###################################################