Files
Spanglish/photo-api/internal/store/access.go
T
MichilisandCursor c9a600b6d6 Add photo galleries API and frontend for public and admin viewing.
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>
2026-07-25 17:32:23 +00:00

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
}