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.
This commit is contained in:
parent
c86d650e5a
commit
315bcae4ca
5 changed files with 338 additions and 12 deletions
68
crud.py
68
crud.py
|
|
@ -12,6 +12,7 @@ from .models import (
|
||||||
ClientTransaction,
|
ClientTransaction,
|
||||||
ClientAnalytics,
|
ClientAnalytics,
|
||||||
UpdateClientSettings,
|
UpdateClientSettings,
|
||||||
|
ClientRegistrationData,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Connect to admin extension's database
|
# Connect to admin extension's database
|
||||||
|
|
@ -447,3 +448,70 @@ async def update_client_dca_settings(client_id: str, settings: UpdateClientSetti
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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
|
||||||
|
|
@ -61,3 +61,10 @@ class UpdateClientSettings(BaseModel):
|
||||||
status: Optional[str] = None # 'active' or 'inactive'
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,18 @@ window.app = Vue.createApp({
|
||||||
delimiters: ['${', '}'],
|
delimiters: ['${', '}'],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
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,
|
dashboardData: null,
|
||||||
transactions: [],
|
transactions: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
|
|
@ -60,6 +72,73 @@ window.app = Vue.createApp({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
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) {
|
formatCurrency(amount) {
|
||||||
if (!amount) return 'Q 0.00';
|
if (!amount) return 'Q 0.00';
|
||||||
// Values are already in full currency units, not centavos
|
// Values are already in full currency units, not centavos
|
||||||
|
|
@ -662,11 +741,18 @@ window.app = Vue.createApp({
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
await Promise.all([
|
|
||||||
this.loadDashboardData(),
|
// Check registration status first
|
||||||
this.loadTransactions(),
|
await this.checkRegistrationStatus()
|
||||||
this.loadChartData()
|
|
||||||
])
|
// Only load dashboard data if registered
|
||||||
|
if (this.isRegistered) {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadDashboardData(),
|
||||||
|
this.loadTransactions(),
|
||||||
|
this.loadChartData()
|
||||||
|
])
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing dashboard:', error)
|
console.error('Error initializing dashboard:', error)
|
||||||
this.error = 'Failed to initialize dashboard'
|
this.error = 'Failed to initialize dashboard'
|
||||||
|
|
@ -694,7 +780,7 @@ window.app = Vue.createApp({
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hasData() {
|
hasData() {
|
||||||
return this.dashboardData && !this.loading
|
return this.dashboardData && !this.loading && this.isRegistered
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<q-card v-if="loading">
|
<q-card v-if="loading || !registrationChecked">
|
||||||
<q-card-section class="text-center">
|
<q-card-section class="text-center">
|
||||||
<q-spinner size="2em" />
|
<q-spinner size="2em" />
|
||||||
<div class="q-mt-md">Loading your DCA dashboard...</div>
|
<div class="q-mt-md">Loading your DCA dashboard...</div>
|
||||||
|
|
@ -26,8 +26,19 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
|
<!-- Not Registered State -->
|
||||||
|
<q-card v-if="registrationChecked && !isRegistered && !loading" class="bg-orange-1">
|
||||||
|
<q-card-section class="text-center">
|
||||||
|
<q-icon name="account_circle" size="3em" color="orange" />
|
||||||
|
<div class="text-h6 q-mt-md text-orange-8">Welcome to Bitcoin DCA!</div>
|
||||||
|
<div class="text-body2 text-grey-7 q-mt-sm">
|
||||||
|
Please complete your registration to start your Dollar Cost Averaging journey.
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
<!-- Dashboard Content -->
|
<!-- Dashboard Content -->
|
||||||
<div v-if="hasData">
|
<div v-if="hasData && isRegistered">
|
||||||
<!-- Hero Card - Bitcoin Stack Progress -->
|
<!-- Hero Card - Bitcoin Stack Progress -->
|
||||||
<q-card class="q-mb-md bg-gradient-to-r from-orange-1 to-orange-2" style="background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%);">
|
<q-card class="q-mb-md bg-gradient-to-r from-orange-1 to-orange-2" style="background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%);">
|
||||||
<q-card-section class="text-center">
|
<q-card-section class="text-center">
|
||||||
|
|
@ -233,8 +244,8 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DCA Performance Chart - Always render to ensure canvas is available -->
|
<!-- DCA Performance Chart - Only show when registered -->
|
||||||
<q-card class="q-mb-md">
|
<q-card v-if="isRegistered" class="q-mb-md">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle2 q-my-none q-mb-md">Bitcoin Accumulation Progress</h6>
|
<h6 class="text-subtitle2 q-my-none q-mb-md">Bitcoin Accumulation Progress</h6>
|
||||||
<div class="chart-container" style="position: relative; height: 300px;">
|
<div class="chart-container" style="position: relative; height: 300px;">
|
||||||
|
|
@ -262,7 +273,7 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
<!-- Dashboard Content -->
|
<!-- Dashboard Content -->
|
||||||
<div v-if="hasData">
|
<div v-if="hasData && isRegistered">
|
||||||
|
|
||||||
<!-- Transaction History -->
|
<!-- Transaction History -->
|
||||||
<q-card>
|
<q-card>
|
||||||
|
|
@ -511,5 +522,107 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Registration Dialog -->
|
||||||
|
<q-dialog v-model="showRegistrationDialog" persistent position="top">
|
||||||
|
<q-card class="q-pa-lg" style="width: 500px; max-width: 90vw">
|
||||||
|
<div class="text-center q-mb-lg">
|
||||||
|
<q-icon name="account_circle" size="4em" color="orange" />
|
||||||
|
<div class="text-h5 q-mt-md text-orange-8">Welcome to DCA!</div>
|
||||||
|
<div class="text-body2 text-grey-7">Let's set up your Bitcoin Dollar Cost Averaging account</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-form @submit="registerClient" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="registrationForm.username"
|
||||||
|
label="Username (Optional)"
|
||||||
|
placeholder="Enter a friendly name"
|
||||||
|
hint="How you'd like to be identified in the system"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="registrationForm.selectedWallet"
|
||||||
|
:options="g.user.wallets"
|
||||||
|
option-label="name"
|
||||||
|
label="DCA Wallet *"
|
||||||
|
hint="Choose which wallet will receive your Bitcoin DCA purchases"
|
||||||
|
>
|
||||||
|
<template v-slot:option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
${scope.opt.name} (ID: ${scope.opt.id.substring(0, 8)}... • ${Math.round(scope.opt.balance_msat / 1000)} sats)
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="registrationForm.dca_mode"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Flow Mode (Recommended)', value: 'flow', description: 'Proportional distribution based on your balance' },
|
||||||
|
{ label: 'Fixed Mode', value: 'fixed', description: 'Set daily purchase limits' }
|
||||||
|
]"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
label="DCA Strategy *"
|
||||||
|
hint="Choose how your Bitcoin purchases will be distributed"
|
||||||
|
>
|
||||||
|
<template v-slot:option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>${scope.opt.label} - ${scope.opt.description}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-if="registrationForm.dca_mode === 'fixed'"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
v-model.number="registrationForm.fixed_mode_daily_limit"
|
||||||
|
label="Daily Limit (GTQ)"
|
||||||
|
placeholder="Enter daily purchase limit in centavos"
|
||||||
|
hint="Maximum amount to purchase per day (in centavos: 1 GTQ = 100 centavos)"
|
||||||
|
:rules="[
|
||||||
|
val => registrationForm.dca_mode !== 'fixed' || (val && val > 0) || 'Daily limit is required for fixed mode'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-banner class="bg-blue-1 text-blue-9 q-mt-md">
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon name="info" color="blue" />
|
||||||
|
</template>
|
||||||
|
<div class="text-caption">
|
||||||
|
<strong>Flow Mode:</strong> Your Bitcoin purchases are proportional to your deposit balance.<br>
|
||||||
|
<strong>Fixed Mode:</strong> Set a daily limit for consistent Bitcoin accumulation.
|
||||||
|
</div>
|
||||||
|
</q-banner>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
:disable="!registrationForm.selectedWallet || !registrationForm.dca_mode"
|
||||||
|
class="full-width"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<q-icon name="rocket_launch" class="q-mr-sm" />
|
||||||
|
Start My DCA Journey
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
52
views_api.py
52
views_api.py
|
|
@ -15,17 +15,69 @@ from .crud import (
|
||||||
get_client_analytics,
|
get_client_analytics,
|
||||||
update_client_dca_settings,
|
update_client_dca_settings,
|
||||||
get_client_by_user_id,
|
get_client_by_user_id,
|
||||||
|
register_dca_client,
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
ClientDashboardSummary,
|
ClientDashboardSummary,
|
||||||
ClientTransaction,
|
ClientTransaction,
|
||||||
ClientAnalytics,
|
ClientAnalytics,
|
||||||
UpdateClientSettings,
|
UpdateClientSettings,
|
||||||
|
ClientRegistrationData,
|
||||||
)
|
)
|
||||||
|
|
||||||
satmachineclient_api_router = APIRouter()
|
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 ###############
|
############## CLIENT DASHBOARD API ###############
|
||||||
###################################################
|
###################################################
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue