- Admin CRUD endpoints for markets with role-based middleware - Anonymous market submission with Cloudflare Turnstile verification - SMTP email notifications on new submissions (LogSender fallback) - Market status workflow (pending/approved/rejected) with admin notes - Nullable location column for submissions without coordinates - CLI tool for promoting users to admin role - Slug generation package extracted from seed - Rate limiting on submission endpoint (3/hour per IP) - Mailpit added to docker-compose for local email testing
77 lines
1.6 KiB
Go
77 lines
1.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"marktvogt.de/backend/internal/config"
|
|
"marktvogt.de/backend/internal/database"
|
|
)
|
|
|
|
func main() {
|
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelInfo,
|
|
})))
|
|
|
|
if len(os.Args) < 3 {
|
|
fmt.Fprintln(os.Stderr, "usage: admin promote <email>")
|
|
os.Exit(1)
|
|
}
|
|
|
|
command := os.Args[1]
|
|
if command != "promote" {
|
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", command)
|
|
os.Exit(1)
|
|
}
|
|
|
|
email := os.Args[2]
|
|
if err := run(email); err != nil {
|
|
slog.Error("admin command failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run(email string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
pool, err := connectDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer pool.Close()
|
|
|
|
tag, err := pool.Exec(ctx, "UPDATE users SET role = 'admin' WHERE email = $1 AND deleted_at IS NULL", email)
|
|
if err != nil {
|
|
return fmt.Errorf("updating user role: %w", err)
|
|
}
|
|
|
|
if tag.RowsAffected() == 0 {
|
|
return fmt.Errorf("no active user found with email %q", email)
|
|
}
|
|
|
|
slog.Info("promoted user to admin", "email", email)
|
|
return nil
|
|
}
|
|
|
|
func connectDB(ctx context.Context) (*pgxpool.Pool, error) {
|
|
if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" {
|
|
pool, err := pgxpool.New(ctx, dbURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("connect via DATABASE_URL: %w", err)
|
|
}
|
|
return pool, nil
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return database.NewPostgres(ctx, cfg.DB)
|
|
}
|