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.db import Database
from lnbits.helpers import urlsafe_short_hash 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: 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: async def delete_event(event_id: str) -> None:
await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id}) 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( await db.execute(
"ALTER TABLE events.events ADD COLUMN nostr_event_created_at INTEGER;" "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 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): class TicketExtra(BaseModel):
applied_promo_code: str | None = None applied_promo_code: str | None = None
sats_paid: int | None = None sats_paid: int | None = None

View file

@ -14,6 +14,9 @@ window.app = Vue.createApp({
allUserEvents: [], allUserEvents: [],
pendingEvents: [], pendingEvents: [],
isAdmin: false, isAdmin: false,
settings: {
auto_approve: false
},
tickets: [], tickets: [],
currencies: [], currencies: [],
eventsTable: { eventsTable: {
@ -117,6 +120,29 @@ window.app = Vue.createApp({
} }
}, },
methods: { 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) { approveEvent(eventId) {
LNbits.utils LNbits.utils
.confirmDialog('Approve this event?') .confirmDialog('Approve this event?')
@ -375,6 +401,7 @@ window.app = Vue.createApp({
this.getTickets() this.getTickets()
this.getEvents() this.getEvents()
this.getPendingEvents() this.getPendingEvents()
this.getSettings()
this.currencies = await LNbits.api.getCurrencies() this.currencies = await LNbits.api.getCurrencies()
} }
} }

View file

@ -2,6 +2,24 @@
%} {% block page %} %} {% block page %}
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<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">
<!-- 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>
<q-card-section> <q-card-section>
<q-btn unelevated color="primary" @click="openEventDialog" <q-btn unelevated color="primary" @click="openEventDialog"

View file

@ -27,14 +27,16 @@ from .crud import (
get_events, get_events,
get_pending_events, get_pending_events,
get_public_events, get_public_events,
get_settings,
get_ticket, get_ticket,
get_tickets, get_tickets,
get_tickets_by_user_id, get_tickets_by_user_id,
# TODO: consider exposing purge_unpaid_tickets via an admin endpoint # TODO: consider exposing purge_unpaid_tickets via an admin endpoint
update_event, update_event,
update_settings,
update_ticket, update_ticket,
) )
from .models import CreateEvent, CreateTicket, Ticket from .models import CreateEvent, CreateTicket, EventsSettings, Ticket
from .nostr_publisher import publish_event_to_nostr from .nostr_publisher import publish_event_to_nostr
from .services import refund_tickets, set_ticket_paid from .services import refund_tickets, set_ticket_paid
@ -130,15 +132,16 @@ async def api_event_create(
else: else:
if not data.wallet: if not data.wallet:
data.wallet = wallet.wallet.id 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 from lnbits.settings import settings
ext_settings = await get_settings()
user_id = wallet.wallet.user user_id = wallet.wallet.user
is_admin = ( is_admin = (
user_id == settings.super_user user_id == settings.super_user
or user_id in settings.lnbits_admin_users 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" data.status = "proposed"
event = await create_event(data) event = await create_event(data)
@ -268,6 +271,26 @@ async def api_event_reject(
return event.dict() 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########## #########Tickets##########