Files
geo/server/internal/tenant/transport/subscription_guard.go
T
root c9267d2fae feat(auth): block disabled user accounts at the subscription guard
RequireActiveTenantSubscription now optionally takes an AuthRepository
and rejects the request with 40311/user_disabled when the actor's user
row is anything other than active. Wire the repo through the tenant
router, surface the new error code in admin-web's
membershipBlockedErrors set, persist a "disabled" membership status when
markStoredMembershipBlocked sees user_disabled, and add localized copy
on the blocked screen and shell membership banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:31:26 +08:00

92 lines
2.3 KiB
Go

package transport
import (
"errors"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5"
"github.com/geo-platform/tenant-api/internal/shared/auth"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
func RequireActiveTenantSubscription(
plans repository.TenantPlanRepository,
authRepos ...repository.AuthRepository,
) gin.HandlerFunc {
return func(c *gin.Context) {
actor, ok := auth.ActorFromCtx(c.Request.Context())
if !ok {
response.Error(c, response.ErrUnauthorized(40100, "not_authenticated", "not authenticated"))
c.Abort()
return
}
if len(authRepos) > 0 && authRepos[0] != nil {
user, err := authRepos[0].GetUserByID(c.Request.Context(), actor.UserID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
response.Error(c, response.ErrNotFound(40401, "user_not_found", "user not found"))
} else {
response.Error(c, response.ErrInternal(50092, "user_lookup_failed", "failed to load user"))
}
c.Abort()
return
}
if !strings.EqualFold(strings.TrimSpace(user.Status), "active") {
response.Error(c, response.ErrForbidden(
40311,
"user_disabled",
"user account is disabled, please contact support",
))
c.Abort()
return
}
}
now := time.Now().UTC()
access, err := plans.GetTenantPlanAccess(c.Request.Context(), actor.TenantID, now)
if err != nil {
response.Error(c, response.ErrInternal(50091, "subscription_lookup_failed", "failed to load tenant subscription"))
c.Abort()
return
}
if access != nil && access.HasActiveAccess(now) {
c.Next()
return
}
reason := ""
if access != nil {
reason = access.BlockedReason(now)
} else {
reason = "subscription_required"
}
switch reason {
case "trial_plan_expired":
response.Error(c, response.ErrForbidden(
40382,
"trial_plan_expired",
"free trial has expired, please contact administrator to reactivate access",
))
case "subscription_required":
response.Error(c, response.ErrForbidden(
40381,
"subscription_required",
"subscription is required, please contact administrator",
))
default:
response.Error(c, response.ErrForbidden(
40383,
"subscription_inactive",
"subscription is inactive, please contact administrator",
))
}
c.Abort()
}
}