Introduces the Go photo-api service, nginx/systemd deploy wiring, and Next.js gallery/lightbox pages so event photos can be managed and browsed. Co-authored-by: Cursor <cursoragent@cursor.com>
114 lines
3.5 KiB
Go
114 lines
3.5 KiB
Go
package store
|
|
|
|
// Every query against Drizzle-owned tables (users, events, tickets,
|
|
// payments) lives in this file so the read-coupling surface stays small
|
|
// and auditable. Column semantics follow backend/src/db/schema.ts.
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type UserAuth struct {
|
|
ID string
|
|
Role string
|
|
TokenVersion int
|
|
AccountStatus string
|
|
}
|
|
|
|
func (db *DB) GetUserAuth(ctx context.Context, userID string) (UserAuth, error) {
|
|
row := db.QueryRowContext(ctx, db.Rebind(
|
|
"SELECT id, role, token_version, account_status FROM users WHERE id = ?"), userID)
|
|
var id, role, tv, status any
|
|
if err := row.Scan(&id, &role, &tv, &status); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return UserAuth{}, ErrNotFound
|
|
}
|
|
return UserAuth{}, err
|
|
}
|
|
return UserAuth{
|
|
ID: asString(id),
|
|
Role: asString(role),
|
|
TokenVersion: int(asInt(tv)),
|
|
AccountStatus: asString(status),
|
|
}, nil
|
|
}
|
|
|
|
// HasPaidTicket implements the review decision: a confirmed/checked-in
|
|
// ticket whose payment is 'paid', or any confirmed/checked-in ticket when
|
|
// the event is free (price = 0).
|
|
func (db *DB) HasPaidTicket(ctx context.Context, userID, eventID string) (bool, error) {
|
|
var v any
|
|
err := db.QueryRowContext(ctx, db.Rebind(`
|
|
SELECT 1
|
|
FROM tickets t
|
|
JOIN events e ON e.id = t.event_id
|
|
LEFT JOIN payments p ON p.ticket_id = t.id
|
|
WHERE t.user_id = ? AND t.event_id = ?
|
|
AND t.status IN ('confirmed','checked_in')
|
|
AND (p.status = 'paid' OR e.price = 0)
|
|
LIMIT 1`), userID, eventID).Scan(&v)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return false, nil
|
|
}
|
|
return err == nil, err
|
|
}
|
|
|
|
type EventSummary struct {
|
|
ID string
|
|
Slug string
|
|
Title string
|
|
TitleEs string
|
|
StartDatetime time.Time
|
|
Status string
|
|
}
|
|
|
|
func (db *DB) GetEventSummary(ctx context.Context, eventID string) (EventSummary, error) {
|
|
return db.getEventWhere(ctx, "id = ?", eventID)
|
|
}
|
|
|
|
// GetEventSummaryBySlug resolves the /events/{slug}/gallery public route.
|
|
func (db *DB) GetEventSummaryBySlug(ctx context.Context, slug string) (EventSummary, error) {
|
|
return db.getEventWhere(ctx, "slug = ?", slug)
|
|
}
|
|
|
|
func (db *DB) getEventWhere(ctx context.Context, where string, arg any) (EventSummary, error) {
|
|
row := db.QueryRowContext(ctx, db.Rebind(
|
|
"SELECT id, slug, title, title_es, start_datetime, status FROM events WHERE "+where), arg)
|
|
var id, slug, title, titleEs, start, status any
|
|
if err := row.Scan(&id, &slug, &title, &titleEs, &start, &status); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return EventSummary{}, ErrNotFound
|
|
}
|
|
return EventSummary{}, err
|
|
}
|
|
return EventSummary{
|
|
ID: asString(id),
|
|
Slug: asString(slug),
|
|
Title: asString(title),
|
|
TitleEs: asString(titleEs),
|
|
StartDatetime: asTime(start),
|
|
Status: asString(status),
|
|
}, nil
|
|
}
|
|
|
|
// CheckMainSchema fails fast at startup if the columns this service reads
|
|
// from Drizzle-owned tables have been renamed or dropped.
|
|
func (db *DB) CheckMainSchema(ctx context.Context) error {
|
|
checks := []string{
|
|
"SELECT id, role, token_version, account_status FROM users WHERE 1 = 0",
|
|
"SELECT id, slug, title, title_es, start_datetime, status, price FROM events WHERE 1 = 0",
|
|
"SELECT id, user_id, event_id, status FROM tickets WHERE 1 = 0",
|
|
"SELECT id, ticket_id, status FROM payments WHERE 1 = 0",
|
|
}
|
|
for _, q := range checks {
|
|
if _, err := db.ExecContext(ctx, q); err != nil {
|
|
return fmt.Errorf("main app schema check failed (%s): %w", q, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|