feat: initial commit — backend API + student cabinet frontend
- Go backend: auth (JWT), points earn/spend, QR token generation, partners, admin grant/stats endpoints with chi router - Next.js 14 frontend: login, student dashboard, transaction history, QR display, partners list - PostgreSQL migrations (4 tables), Redis cache, Docker Compose - CORS middleware, role-based route protection, Zustand auth store Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/cu-points/backend/internal/admin"
|
||||
"github.com/cu-points/backend/internal/auth"
|
||||
"github.com/cu-points/backend/internal/config"
|
||||
"github.com/cu-points/backend/internal/middleware"
|
||||
"github.com/cu-points/backend/internal/partners"
|
||||
"github.com/cu-points/backend/internal/points"
|
||||
"github.com/cu-points/backend/internal/users"
|
||||
cachepkg "github.com/cu-points/backend/pkg/cache"
|
||||
dbpkg "github.com/cu-points/backend/pkg/db"
|
||||
)
|
||||
|
||||
func main() {
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
slog.Error("config load failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
db, err := dbpkg.NewPool(ctx, cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
slog.Error("postgres connect failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
redisClient, err := cachepkg.NewClient(ctx, cfg.RedisURL)
|
||||
if err != nil {
|
||||
slog.Error("redis connect failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer redisClient.Close()
|
||||
|
||||
slog.Info("infrastructure connected", "postgres", cfg.DatabaseURL, "redis", cfg.RedisURL)
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||
jwtManager := auth.NewJWTManager(cfg.JWTSecret, cfg.JWTAccessTTL, cfg.JWTRefreshTTL)
|
||||
authRepo := auth.NewRepository(db)
|
||||
authSvc := auth.NewService(authRepo, jwtManager)
|
||||
authHandler := auth.NewHandler(authSvc)
|
||||
|
||||
// ── Users ─────────────────────────────────────────────────────────────────
|
||||
usersRepo := users.NewRepository(db)
|
||||
usersSvc := users.NewService(usersRepo)
|
||||
usersHandler := users.NewHandler(usersSvc)
|
||||
|
||||
// ── Partners ──────────────────────────────────────────────────────────────
|
||||
partnersRepo := partners.NewRepository(db)
|
||||
partnersSvc := partners.NewService(partnersRepo)
|
||||
partnersHandler := partners.NewHandler(partnersSvc)
|
||||
|
||||
// ── Points ────────────────────────────────────────────────────────────────
|
||||
pointsRepo := points.NewRepository(db)
|
||||
pointsCache := points.NewRedisCache(redisClient)
|
||||
// pointsSvc is also passed to adminSvc so GrantPoints reuses EarnPoints logic.
|
||||
pointsSvc := points.NewService(pointsRepo, pointsCache, cfg.JWTSecret)
|
||||
pointsHandler := points.NewHandler(pointsSvc)
|
||||
|
||||
// ── Admin ─────────────────────────────────────────────────────────────────
|
||||
adminSvc := admin.NewService(db, pointsSvc)
|
||||
adminHandler := admin.NewHandler(adminSvc)
|
||||
|
||||
// ── Router ────────────────────────────────────────────────────────────────
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.CORS)
|
||||
r.Use(middleware.Logger)
|
||||
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
// Public — no authentication required.
|
||||
r.Post("/auth/login", authHandler.Login)
|
||||
r.Post("/auth/refresh", authHandler.Refresh)
|
||||
r.Get("/partners", partnersHandler.List)
|
||||
|
||||
// Any authenticated user — profile endpoint used by the login flow for all roles.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.Auth(cfg.JWTSecret))
|
||||
|
||||
r.Get("/me", usersHandler.Me)
|
||||
})
|
||||
|
||||
// Student-only endpoints.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.Auth(cfg.JWTSecret))
|
||||
r.Use(middleware.RequireRole("student"))
|
||||
|
||||
r.Get("/me/transactions", usersHandler.Transactions)
|
||||
r.Get("/me/qr", pointsHandler.GenerateQR)
|
||||
})
|
||||
|
||||
// Partner-only endpoints.
|
||||
// Rate-limited to 10 spend requests per minute per partner to prevent abuse.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.Auth(cfg.JWTSecret))
|
||||
r.Use(middleware.RequireRole("partner"))
|
||||
r.Use(middleware.SpendRateLimit(redisClient, 10))
|
||||
|
||||
r.Post("/partner/spend", pointsHandler.Spend)
|
||||
})
|
||||
|
||||
// Admin-only endpoints.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.Auth(cfg.JWTSecret))
|
||||
r.Use(middleware.RequireRole("admin"))
|
||||
|
||||
r.Post("/admin/points/grant", adminHandler.GrantPoints)
|
||||
r.Get("/admin/transactions", adminHandler.ListTransactions)
|
||||
r.Get("/admin/users", adminHandler.ListUsers)
|
||||
r.Get("/admin/stats", adminHandler.Stats)
|
||||
})
|
||||
})
|
||||
|
||||
// Health check — no auth required.
|
||||
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"status":"ok"}`)
|
||||
})
|
||||
|
||||
addr := ":" + cfg.Port
|
||||
slog.Info("server starting", "addr", addr)
|
||||
if err := http.ListenAndServe(addr, r); err != nil {
|
||||
slog.Error("server failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user