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
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:
86
services/context/subscription.go
Normal file
86
services/context/subscription.go
Normal 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)
|
||||
}
|
||||
}
|
||||
47
services/forms/monetize_form.go
Normal file
47
services/forms/monetize_form.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user