mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
refactor(db): replace sqlc with handwritten pgx repos
- Finish Paddle-to-Stripe billing migration; drop paddle/stripe libs - Renumber migrations; add 00012 memberships+manage_token, 00013 location_type_capacity - Add tests: admin, auth, bookings customer service - Remove graphify-out artifacts and stale env files Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
2039669e2c
commit
f2f7439050
93 files changed
+6987
-69060
No files matched your search
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bookra/apps/backend/internal/auth"
|
||||
"bookra/apps/backend/internal/db"
|
||||
"bookra/apps/backend/internal/domain"
|
||||
|
||||
@@ -21,9 +21,9 @@ var (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo db.Repository
|
||||
adminEmail string
|
||||
adminKey string
|
||||
repo db.Repository
|
||||
adminEmail string
|
||||
adminKey string
|
||||
}
|
||||
|
||||
func NewService(repo db.Repository, adminEmail, adminKey string) *Service {
|
||||
@@ -52,20 +52,39 @@ func (s *Service) ValidateAdminLogin(email, key string) bool {
|
||||
return emailMatch && keyMatch
|
||||
}
|
||||
|
||||
// RequireAdmin is middleware that checks for admin authentication
|
||||
// It supports two modes:
|
||||
// 1. Admin credentials via X-Admin-Email and X-Admin-Key headers (for API access)
|
||||
// 2. Session-based auth where the user has role "admin" or "superadmin"
|
||||
func RequireAdmin(adminSvc *Service, authSvc interface{ IsAdmin(ctx context.Context, userID string) (bool, error) }) gin.HandlerFunc {
|
||||
const adminActorKey = "adminActor"
|
||||
|
||||
// ActorFromContext returns the admin identity recorded by RequireAdmin.
|
||||
func ActorFromContext(c *gin.Context) string {
|
||||
value, ok := c.Get(adminActorKey)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
actor, _ := value.(string)
|
||||
return actor
|
||||
}
|
||||
|
||||
// RequireAdmin is middleware that checks for admin authentication.
|
||||
// It supports three modes:
|
||||
// 1. Admin credentials via X-Admin-Email and X-Admin-Key headers (for API access)
|
||||
// 2. Bearer tokens verified against the auth verifier, where the token subject
|
||||
// maps to a user with role "admin" or "superadmin"
|
||||
// 3. Demo mode, where the demo owner is treated as admin
|
||||
func RequireAdmin(adminSvc *Service, verifier *auth.Verifier, repo db.Repository, demoMode bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if demoMode {
|
||||
c.Set(adminActorKey, auth.DemoPrincipal.Subject)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Check for admin header credentials (direct admin login)
|
||||
adminEmail := c.GetHeader("X-Admin-Email")
|
||||
adminKey := c.GetHeader("X-Admin-Key")
|
||||
|
||||
if adminEmail != "" && adminKey != "" {
|
||||
if adminEmail != "" || adminKey != "" {
|
||||
if adminSvc.ValidateAdminLogin(adminEmail, adminKey) {
|
||||
c.Set("isAdmin", true)
|
||||
c.Set("adminMode", "credentials")
|
||||
c.Set(adminActorKey, adminEmail)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -74,31 +93,39 @@ func RequireAdmin(adminSvc *Service, authSvc interface{ IsAdmin(ctx context.Cont
|
||||
}
|
||||
|
||||
// Check for Bearer token with admin role
|
||||
auth := c.GetHeader("Authorization")
|
||||
if auth != "" && strings.HasPrefix(auth, "Bearer ") {
|
||||
// The auth middleware should have already validated the token
|
||||
// and set the user info in context
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin, err := authSvc.IsAdmin(c.Request.Context(), userID.(string))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to check admin status"})
|
||||
return
|
||||
}
|
||||
|
||||
if isAdmin {
|
||||
c.Set("isAdmin", true)
|
||||
c.Set("adminMode", "session")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
tokenString, ok := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer ")
|
||||
if !ok || strings.TrimSpace(tokenString) == "" {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin access required"})
|
||||
return
|
||||
}
|
||||
if verifier == nil || !verifier.Enabled() {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "auth_not_configured"})
|
||||
return
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin access required"})
|
||||
claims, err := verifier.Verify(strings.TrimSpace(tokenString))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid_token"})
|
||||
return
|
||||
}
|
||||
subject, _ := claims["sub"].(string)
|
||||
if strings.TrimSpace(subject) == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid_token_subject"})
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin, err := repo.IsAdminSubject(c.Request.Context(), subject)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to check admin status"})
|
||||
return
|
||||
}
|
||||
if !isAdmin {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin access required"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(adminActorKey, subject)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,11 +137,11 @@ func (s *Service) GetDashboardStats(ctx context.Context) (domain.AdminDashboardS
|
||||
}
|
||||
|
||||
return domain.AdminDashboardStats{
|
||||
TotalTenants: stats.TotalTenants,
|
||||
TotalUsers: stats.TotalUsers,
|
||||
ActiveSubscriptions: stats.ActiveSubscriptions,
|
||||
TrialSubscriptions: stats.TrialSubscriptions,
|
||||
BookingsThisMonth: stats.BookingsThisMonth,
|
||||
TotalTenants: stats.TotalTenants,
|
||||
TotalUsers: stats.TotalUsers,
|
||||
ActiveSubscriptions: stats.ActiveSubscriptions,
|
||||
TrialSubscriptions: stats.TrialSubscriptions,
|
||||
BookingsThisMonth: stats.BookingsThisMonth,
|
||||
RevenueThisMonthCents: stats.RevenueThisMonth,
|
||||
}, nil
|
||||
}
|
||||
@@ -144,12 +171,12 @@ func (s *Service) ListTenants(ctx context.Context, page, pageSize int) (domain.A
|
||||
|
||||
for i, t := range tenants {
|
||||
result.Tenants[i] = domain.AdminTenant{
|
||||
ID: t.ID,
|
||||
Slug: t.Slug,
|
||||
Name: t.Name,
|
||||
PlanCode: t.PlanCode,
|
||||
ID: t.ID,
|
||||
Slug: t.Slug,
|
||||
Name: t.Name,
|
||||
PlanCode: t.PlanCode,
|
||||
SubscriptionStatus: t.SubscriptionStatus,
|
||||
BillingProvider: t.BillingProvider,
|
||||
BillingProvider: t.BillingProvider,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +225,8 @@ func (s *Service) ListUsers(ctx context.Context, page, pageSize int) (domain.Adm
|
||||
func (s *Service) UpdateUserRole(ctx context.Context, adminUserID, targetUserID, newRole string, ip, userAgent string) error {
|
||||
// Validate role
|
||||
validRoles := map[string]bool{
|
||||
"user": true,
|
||||
"admin": true,
|
||||
"user": true,
|
||||
"admin": true,
|
||||
"superadmin": true,
|
||||
}
|
||||
if !validRoles[newRole] {
|
||||
@@ -224,20 +251,9 @@ func (s *Service) UpdateUserRole(ctx context.Context, adminUserID, targetUserID,
|
||||
})
|
||||
}
|
||||
|
||||
// SyncTenantSubscription manually syncs a tenant's subscription from Stripe
|
||||
func (s *Service) SyncTenantSubscription(ctx context.Context, tenantID string) error {
|
||||
// This will be called from the billing service
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringPtrToStr(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Ensure time package is imported
|
||||
_ = time.Now()
|
||||
}
|
||||
Reference in new issue
Block a user