feat: event proposal and approval workflow #9

Closed
padreug wants to merge 38 commits from feat/event-approval-workflow into main
6 changed files with 116 additions and 4 deletions
Showing only changes of commit d69ec7dda2 - Show all commits

feat: add admin-toggleable auto-approve setting
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>
Padreug 2026-04-27 17:59:59 +02:00

23
crud.py
View file

@ -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})

View file

@ -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);"
)

View file

@ -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

View file

@ -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()
}
}

View file

@ -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"

View file

@ -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##########