2
0

feat(ci): add repository subscription monetization system
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m10s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m13s
Build and Release / Lint (push) Successful in 5m25s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m13s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h5m42s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m30s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m55s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m36s

Implement complete subscription monetization system for repositories with Stripe and PayPal integration. Includes:
- Database models and migrations for monetization settings, subscription products, and user subscriptions
- Payment provider abstraction layer with Stripe and PayPal implementations
- Admin UI for configuring payment providers and viewing subscriptions
- Repository settings UI for managing subscription products and tiers
- Subscription checkout flow and webhook handlers for payment events
- Access control to gate repository code behind active subscriptions
This commit is contained in:
2026-01-31 13:37:07 -05:00
parent 7ca828cb94
commit d1f20f6b46
40 changed files with 2668 additions and 12 deletions

View File

@@ -0,0 +1,86 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"net/http"
monetize_model "code.gitcaddy.com/server/v3/models/monetize"
perm_model "code.gitcaddy.com/server/v3/models/perm"
"code.gitcaddy.com/server/v3/modules/setting"
"code.gitcaddy.com/server/v3/modules/templates"
)
const tplSubscribe templates.TplName = "repo/subscribe"
// RequireSubscriptionForCode returns middleware that gates code access behind a paid subscription.
// It checks:
// 1. Is monetization enabled globally?
// 2. Does this repo have subscriptions enabled?
// 3. Is the user the owner, admin, or collaborator with write access? (bypass)
// 4. Is the user a site admin? (bypass)
// 5. Does the user have an active subscription? If not, show the subscribe page.
func RequireSubscriptionForCode() func(ctx *Context) {
return func(ctx *Context) {
if !setting.Monetize.Enabled {
return
}
if ctx.Repo.Repository == nil || !ctx.Repo.Repository.SubscriptionsEnabled {
return
}
// Bypass for owner, admin, and collaborators with write+ access
if ctx.Repo.IsOwner() || ctx.Repo.IsAdmin() {
return
}
if ctx.Repo.Permission.AccessMode >= perm_model.AccessModeWrite {
return
}
// Bypass for site admins
if ctx.Doer != nil && ctx.Doer.IsAdmin {
return
}
// Bypass for unauthenticated users on public repos — they see the subscribe prompt
if ctx.Doer == nil {
ctx.Redirect(ctx.Repo.RepoLink + "/subscribe")
return
}
// Check if user has an active subscription
hasAccess, err := monetize_model.HasActiveSubscription(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("HasActiveSubscription", err)
return
}
if hasAccess {
return
}
// Show subscribe page with HTTP 402
ctx.Data["Title"] = ctx.Tr("repo.subscribe.title")
products, err := monetize_model.GetActiveProductsByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetActiveProductsByRepoID", err)
return
}
ctx.Data["Products"] = products
settings, err := monetize_model.GetSetting(ctx)
if err != nil {
ctx.ServerError("GetSetting", err)
return
}
ctx.Data["StripeEnabled"] = settings.StripeEnabled
ctx.Data["StripePublishableKey"] = settings.StripePublishableKey
ctx.Data["PayPalEnabled"] = settings.PayPalEnabled
ctx.Data["PayPalClientID"] = settings.PayPalClientID
ctx.Data["PayPalSandbox"] = settings.PayPalSandbox
ctx.HTML(http.StatusPaymentRequired, tplSubscribe)
}
}

View File

@@ -0,0 +1,47 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package forms
import (
"net/http"
"code.gitcaddy.com/server/v3/modules/web/middleware"
"code.gitcaddy.com/server/v3/services/context"
"gitea.com/go-chi/binding"
)
// MonetizeSettingsForm is the admin form for configuring payment providers.
type MonetizeSettingsForm struct {
StripeEnabled bool
StripeSecretKey string `binding:"MaxSize(255)"`
StripePublishableKey string `binding:"MaxSize(255)"`
StripeWebhookSecret string `binding:"MaxSize(255)"`
PayPalEnabled bool
PayPalClientID string `binding:"MaxSize(255)"`
PayPalClientSecret string `binding:"MaxSize(255)"`
PayPalWebhookID string `binding:"MaxSize(255)"`
PayPalSandbox bool
}
// Validate validates the fields
func (f *MonetizeSettingsForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// SubscriptionProductForm is the repo settings form for creating/editing products.
type SubscriptionProductForm struct {
Name string `binding:"Required;MaxSize(255)"`
Type int `binding:"Required;Range(1,3)"`
PriceCents int64 `binding:"Required;Min(1)"`
Currency string `binding:"Required;MaxSize(3)"`
IsActive bool
}
// Validate validates the fields
func (f *SubscriptionProductForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}