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,
|
||||
ClientAnalytics,
|
||||
UpdateClientSettings,
|
||||
ClientRegistrationData,
|
||||
)
|
||||
|
||||
# Connect to admin extension's database
|
||||
|
|
@ -447,3 +448,70 @@ async def update_client_dca_settings(client_id: str, settings: UpdateClientSetti
|
|||
return True
|
||||
except Exception:
|
||||
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'
|
||||
|
||||
|
||||
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: ['${', '}'],
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
|
||||
<!-- Loading State -->
|
||||
<q-card v-if="loading">
|
||||
<q-card v-if="loading || !registrationChecked">
|
||||
<q-card-section class="text-center">
|
||||
<q-spinner size="2em" />
|
||||
<div class="q-mt-md">Loading your DCA dashboard...</div>
|
||||
|
|
@ -26,8 +26,19 @@
|
|||
</q-card-section>
|
||||
</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 -->
|
||||
<div v-if="hasData">
|
||||
<div v-if="hasData && isRegistered">
|
||||
<!-- 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-section class="text-center">
|
||||
|
|
@ -233,8 +244,8 @@
|
|||
|
||||
</div>
|
||||
|
||||
<!-- DCA Performance Chart - Always render to ensure canvas is available -->
|
||||
<q-card class="q-mb-md">
|
||||
<!-- DCA Performance Chart - Only show when registered -->
|
||||
<q-card v-if="isRegistered" class="q-mb-md">
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle2 q-my-none q-mb-md">Bitcoin Accumulation Progress</h6>
|
||||
<div class="chart-container" style="position: relative; height: 300px;">
|
||||
|
|
@ -262,7 +273,7 @@
|
|||
</q-card>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
<div v-if="hasData">
|
||||
<div v-if="hasData && isRegistered">
|
||||
|
||||
<!-- Transaction History -->
|
||||
<q-card>
|
||||
|
|
@ -511,5 +522,107 @@
|
|||
</q-card-section>
|
||||
</q-card>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
|
|
|||
52
views_api.py
52
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 ###############
|
||||
###################################################
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue