feat: event proposal and approval workflow #9
6 changed files with 116 additions and 4 deletions
feat: add admin-toggleable auto-approve setting
Some checks failed
lint.yml / feat: add admin-toggleable auto-approve setting (pull_request) Failing after 0s
Some checks failed
lint.yml / feat: add admin-toggleable auto-approve setting (pull_request) Failing after 0s
- Extension settings table with auto_approve boolean - GET/PUT /api/v1/settings endpoints (LNbits admin only) - Settings card in admin UI with toggle - When auto_approve is enabled, non-admin events skip approval Closes aiolabs/events#11 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
commit
d69ec7dda2
23
crud.py
23
crud.py
|
|
@ -5,7 +5,7 @@ from typing import Optional
|
|||
from lnbits.db import Database
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import CreateEvent, Event, Ticket, TicketExtra
|
||||
from .models import CreateEvent, Event, EventsSettings, Ticket, TicketExtra
|
||||
|
||||
|
||||
def _parse_ticket_row(row) -> dict:
|
||||
|
|
@ -220,6 +220,27 @@ async def get_pending_events() -> list[Event]:
|
|||
)
|
||||
|
||||
|
||||
async def get_settings() -> EventsSettings:
|
||||
"""Get extension settings (single row, always exists after migration)."""
|
||||
row = await db.fetchone("SELECT * FROM events.settings WHERE id = 1")
|
||||
if row:
|
||||
return EventsSettings(**dict(row))
|
||||
return EventsSettings()
|
||||
|
||||
|
||||
async def update_settings(settings: EventsSettings) -> EventsSettings:
|
||||
"""Update extension settings."""
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE events.settings
|
||||
SET auto_approve = :auto_approve
|
||||
WHERE id = 1
|
||||
""",
|
||||
{"auto_approve": settings.auto_approve},
|
||||
)
|
||||
return settings
|
||||
|
||||
|
||||
async def delete_event(event_id: str) -> None:
|
||||
await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id})
|
||||
|
||||
|
|
|
|||
|
|
@ -214,3 +214,20 @@ async def m009_add_nostr_columns(db):
|
|||
await db.execute(
|
||||
"ALTER TABLE events.events ADD COLUMN nostr_event_created_at INTEGER;"
|
||||
)
|
||||
|
||||
|
||||
async def m010_add_events_settings(db):
|
||||
"""
|
||||
Create extension settings table for admin-configurable options.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS events.settings (
|
||||
id INTEGER PRIMARY KEY DEFAULT 1,
|
||||
auto_approve BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"INSERT OR IGNORE INTO events.settings (id, auto_approve) VALUES (1, FALSE);"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,12 @@ class Event(BaseModel):
|
|||
nostr_event_created_at: int | None = None
|
||||
|
||||
|
||||
class EventsSettings(BaseModel):
|
||||
"""Extension-level settings for the events extension."""
|
||||
|
||||
auto_approve: bool = False # Skip approval for all users
|
||||
|
||||
|
||||
class TicketExtra(BaseModel):
|
||||
applied_promo_code: str | None = None
|
||||
sats_paid: int | None = None
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ window.app = Vue.createApp({
|
|||
allUserEvents: [],
|
||||
pendingEvents: [],
|
||||
isAdmin: false,
|
||||
settings: {
|
||||
auto_approve: false
|
||||
},
|
||||
tickets: [],
|
||||
currencies: [],
|
||||
eventsTable: {
|
||||
|
|
@ -117,6 +120,29 @@ window.app = Vue.createApp({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
getSettings() {
|
||||
LNbits.api
|
||||
.request('GET', '/events/api/v1/settings')
|
||||
.then(response => {
|
||||
this.settings = response.data
|
||||
})
|
||||
.catch(() => {
|
||||
// Not admin or settings not available
|
||||
})
|
||||
},
|
||||
saveSettings() {
|
||||
LNbits.api
|
||||
.request('PUT', '/events/api/v1/settings', null, this.settings)
|
||||
.then(() => {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Settings saved'
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
approveEvent(eventId) {
|
||||
LNbits.utils
|
||||
.confirmDialog('Approve this event?')
|
||||
|
|
@ -375,6 +401,7 @@ window.app = Vue.createApp({
|
|||
this.getTickets()
|
||||
this.getEvents()
|
||||
this.getPendingEvents()
|
||||
this.getSettings()
|
||||
this.currencies = await LNbits.api.getCurrencies()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,24 @@
|
|||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<!-- Settings (admin only) -->
|
||||
<q-card v-if="isAdmin">
|
||||
<q-card-section>
|
||||
<div class="row items-center justify-between">
|
||||
<div class="col">
|
||||
<span class="text-subtitle1">Settings</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-toggle
|
||||
v-model="settings.auto_approve"
|
||||
label="Auto-approve events"
|
||||
@update:model-value="saveSettings"
|
||||
></q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="primary" @click="openEventDialog"
|
||||
|
|
|
|||
29
views_api.py
29
views_api.py
|
|
@ -27,14 +27,16 @@ from .crud import (
|
|||
get_events,
|
||||
get_pending_events,
|
||||
get_public_events,
|
||||
get_settings,
|
||||
get_ticket,
|
||||
get_tickets,
|
||||
get_tickets_by_user_id,
|
||||
# TODO: consider exposing purge_unpaid_tickets via an admin endpoint
|
||||
update_event,
|
||||
update_settings,
|
||||
update_ticket,
|
||||
)
|
||||
from .models import CreateEvent, CreateTicket, Ticket
|
||||
from .models import CreateEvent, CreateTicket, EventsSettings, Ticket
|
||||
from .nostr_publisher import publish_event_to_nostr
|
||||
from .services import refund_tickets, set_ticket_paid
|
||||
|
||||
|
|
@ -130,15 +132,16 @@ async def api_event_create(
|
|||
else:
|
||||
if not data.wallet:
|
||||
data.wallet = wallet.wallet.id
|
||||
# Auto-approve for LNbits admins, require approval for regular users
|
||||
# Check if approval is required for non-admin users
|
||||
from lnbits.settings import settings
|
||||
|
||||
ext_settings = await get_settings()
|
||||
user_id = wallet.wallet.user
|
||||
is_admin = (
|
||||
user_id == settings.super_user
|
||||
or user_id in settings.lnbits_admin_users
|
||||
)
|
||||
if not is_admin:
|
||||
if not is_admin and not ext_settings.auto_approve:
|
||||
data.status = "proposed"
|
||||
event = await create_event(data)
|
||||
|
||||
|
|
@ -268,6 +271,26 @@ async def api_event_reject(
|
|||
return event.dict()
|
||||
|
||||
|
||||
#########Settings##########
|
||||
|
||||
|
||||
@events_api_router.get("/api/v1/settings")
|
||||
async def api_get_settings(
|
||||
admin: Account = Depends(check_admin),
|
||||
) -> EventsSettings:
|
||||
"""Get extension settings. LNbits admin only."""
|
||||
return await get_settings()
|
||||
|
||||
|
||||
@events_api_router.put("/api/v1/settings")
|
||||
async def api_update_settings(
|
||||
data: EventsSettings,
|
||||
admin: Account = Depends(check_admin),
|
||||
) -> EventsSettings:
|
||||
"""Update extension settings. LNbits admin only."""
|
||||
return await update_settings(data)
|
||||
|
||||
|
||||
#########Tickets##########
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue