From 18e81f4936976b2172bb6f52c06dade86140f24c Mon Sep 17 00:00:00 2001 From: rxdn <29165304+rxdn@users.noreply.github.com> Date: Sun, 1 Sep 2024 17:00:56 +0100 Subject: [PATCH] Create interface for selecting premium guilds --- .../endpoints/api/premium/activeguilds.go | 162 ++++++++++++++++ .../endpoints/api/premium/entitlements.go | 70 +++++++ app/http/server.go | 25 ++- frontend/src/routes.js | 7 + .../src/views/premium/SelectServers.svelte | 183 ++++++++++++++++++ go.mod | 2 +- go.sum | 2 + 7 files changed, 442 insertions(+), 9 deletions(-) create mode 100644 app/http/endpoints/api/premium/activeguilds.go create mode 100644 app/http/endpoints/api/premium/entitlements.go diff --git a/app/http/endpoints/api/premium/activeguilds.go b/app/http/endpoints/api/premium/activeguilds.go new file mode 100644 index 0000000..6970870 --- /dev/null +++ b/app/http/endpoints/api/premium/activeguilds.go @@ -0,0 +1,162 @@ +package api + +import ( + dbclient "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/GoPanel/utils/types" + "github.com/TicketsBot/common/model" + "github.com/TicketsBot/common/permission" + "github.com/TicketsBot/common/premium" + "github.com/gin-gonic/gin" + "net/http" +) + +type setActiveGuildsBody struct { + SelectedGuilds types.UInt64StringSlice `json:"selected_guilds"` +} + +func SetActiveGuilds(ctx *gin.Context) { + userId := ctx.Keys["userid"].(uint64) + + var body setActiveGuildsBody + if err := ctx.ShouldBindJSON(&body); err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorJson(err)) + return + } + + legacyEntitlement, err := dbclient.Client.LegacyPremiumEntitlements.GetUserTier(ctx, userId, premium.PatreonGracePeriod) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + if legacyEntitlement == nil || legacyEntitlement.IsLegacy { + ctx.JSON(http.StatusBadRequest, utils.ErrorStr("Not a premium user")) + return + } + + tx, err := dbclient.Client.BeginTx(ctx) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + defer tx.Rollback(ctx) + + // Validate under the limit + limit, ok, err := dbclient.Client.MultiServerSkus.GetPermittedServerCount(ctx, tx, legacyEntitlement.SkuId) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + if !ok { + ctx.JSON(http.StatusBadRequest, utils.ErrorStr("Not a multi-server subscription")) + return + } + + if len(body.SelectedGuilds) > limit { + ctx.JSON(http.StatusBadRequest, utils.ErrorStr("Too many guilds selected")) + return + } + + // Validate has admin in each server + for _, guildId := range body.SelectedGuilds { + permissionLevel, err := utils.GetPermissionLevel(ctx, guildId, userId) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + if permissionLevel < permission.Admin { + ctx.JSON(http.StatusForbidden, utils.ErrorStr("Missing permissions in guild %d", guildId)) + return + } + } + + existingGuildEntitlements, err := dbclient.Client.LegacyPremiumEntitlementGuilds.ListForUser(ctx, tx, userId) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + // Remove entitlements from guilds that are no longer selected + for _, existingEntitlement := range existingGuildEntitlements { + if !utils.Contains(body.SelectedGuilds, existingEntitlement.GuildId) { + if err := dbclient.Client.LegacyPremiumEntitlementGuilds.DeleteByEntitlement(ctx, tx, existingEntitlement.EntitlementId); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + if err := dbclient.Client.Entitlements.DeleteById(ctx, tx, existingEntitlement.EntitlementId); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + } + } + + // Create entitlements for guilds that were not previously selected, but now are + existingGuildIds := make([]uint64, len(existingGuildEntitlements)) + for i, existingEntitlement := range existingGuildEntitlements { + existingGuildIds[i] = existingEntitlement.GuildId + } + + for _, guildId := range body.SelectedGuilds { + if !utils.Contains(existingGuildIds, guildId) { + created, err := dbclient.Client.Entitlements.Create(ctx, tx, &guildId, &userId, legacyEntitlement.SkuId, model.EntitlementSourcePatreon, nil) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + if err := dbclient.Client.LegacyPremiumEntitlementGuilds.Insert(ctx, tx, userId, guildId, created.Id); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + } + } + + // Update entitlements for guilds that were previously selected and still are. This may involve recreating the + // entitlement if the SKU has changed. + for _, existingEntitlement := range existingGuildEntitlements { + if utils.Contains(body.SelectedGuilds, existingEntitlement.GuildId) { + entitlement, err := dbclient.Client.Entitlements.GetById(ctx, tx, existingEntitlement.EntitlementId) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + if entitlement == nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorStr("Entitlement %s not found", existingEntitlement.EntitlementId.String())) + return + } + + if entitlement.SkuId == legacyEntitlement.SkuId { + continue + } else { + // If we need to switch the SKU, then delete and recreate the entitlement + if err := dbclient.Client.LegacyPremiumEntitlementGuilds.DeleteByEntitlement(ctx, tx, existingEntitlement.EntitlementId); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + if err := dbclient.Client.Entitlements.DeleteById(ctx, tx, existingEntitlement.EntitlementId); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + if _, err := dbclient.Client.Entitlements.Create(ctx, tx, &existingEntitlement.GuildId, &userId, legacyEntitlement.SkuId, model.EntitlementSourcePatreon, entitlement.ExpiresAt); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + } + } + } + + if err := tx.Commit(ctx); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/app/http/endpoints/api/premium/entitlements.go b/app/http/endpoints/api/premium/entitlements.go new file mode 100644 index 0000000..56da6ed --- /dev/null +++ b/app/http/endpoints/api/premium/entitlements.go @@ -0,0 +1,70 @@ +package api + +import ( + dbclient "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/GoPanel/utils/types" + "github.com/TicketsBot/common/premium" + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v4" + "net/http" +) + +func GetEntitlements(ctx *gin.Context) { + userId := ctx.Keys["userid"].(uint64) + + entitlements, err := dbclient.Client.Entitlements.ListUserSubscriptions(ctx, userId, premium.GracePeriod) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + legacyEntitlement, err := dbclient.Client.LegacyPremiumEntitlements.GetUserTier(ctx, userId, premium.GracePeriod) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + res := gin.H{ + "entitlements": entitlements, + "legacy_entitlement": legacyEntitlement, + } + + if legacyEntitlement == nil || legacyEntitlement.IsLegacy { + ctx.JSON(http.StatusOK, res) + return + } + + // If it's a multi-server subscription, fetch more data + var permitted *int + guildIds := make([]uint64, 0) + if err := dbclient.Client.WithTx(ctx, func(tx pgx.Tx) error { + tmp, ok, err := dbclient.Client.MultiServerSkus.GetPermittedServerCount(ctx, tx, legacyEntitlement.SkuId) + if err != nil { + return err + } + + if ok { + permitted = &tmp + } + + activeEntitlements, err := dbclient.Client.LegacyPremiumEntitlementGuilds.ListForUser(ctx, tx, userId) + if err != nil { + return err + } + + for _, entitlement := range activeEntitlements { + guildIds = append(guildIds, entitlement.GuildId) + } + + return nil + }); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorJson(err)) + return + } + + res["permitted_server_count"] = permitted + res["selected_guilds"] = types.UInt64StringSlice(guildIds) + + ctx.JSON(http.StatusOK, res) +} diff --git a/app/http/server.go b/app/http/server.go index 0e28e6e..93c0c91 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -7,6 +7,7 @@ import ( api_forms "github.com/TicketsBot/GoPanel/app/http/endpoints/api/forms" api_integrations "github.com/TicketsBot/GoPanel/app/http/endpoints/api/integrations" api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel" + api_premium "github.com/TicketsBot/GoPanel/app/http/endpoints/api/premium" api_settings "github.com/TicketsBot/GoPanel/app/http/endpoints/api/settings" api_override "github.com/TicketsBot/GoPanel/app/http/endpoints/api/staffoverride" api_tags "github.com/TicketsBot/GoPanel/app/http/endpoints/api/tags" @@ -79,15 +80,23 @@ func StartServer(sm *livechat.SocketManager) { { apiGroup.GET("/session", api.SessionHandler) - integrationGroup := apiGroup.Group("/integrations") + { + integrationGroup := apiGroup.Group("/integrations") - integrationGroup.GET("/self", api_integrations.GetOwnedIntegrationsHandler) - integrationGroup.GET("/view/:integrationid", api_integrations.GetIntegrationHandler) - integrationGroup.GET("/view/:integrationid/detail", api_integrations.GetIntegrationDetailedHandler) - integrationGroup.POST("/:integrationid/public", api_integrations.SetIntegrationPublicHandler) - integrationGroup.PATCH("/:integrationid", api_integrations.UpdateIntegrationHandler) - integrationGroup.DELETE("/:integrationid", api_integrations.DeleteIntegrationHandler) - apiGroup.POST("/integrations", api_integrations.CreateIntegrationHandler) + integrationGroup.GET("/self", api_integrations.GetOwnedIntegrationsHandler) + integrationGroup.GET("/view/:integrationid", api_integrations.GetIntegrationHandler) + integrationGroup.GET("/view/:integrationid/detail", api_integrations.GetIntegrationDetailedHandler) + integrationGroup.POST("/:integrationid/public", api_integrations.SetIntegrationPublicHandler) + integrationGroup.PATCH("/:integrationid", api_integrations.UpdateIntegrationHandler) + integrationGroup.DELETE("/:integrationid", api_integrations.DeleteIntegrationHandler) + apiGroup.POST("/integrations", api_integrations.CreateIntegrationHandler) + } + + { + premiumGroup := apiGroup.Group("/premium/@me") + premiumGroup.GET("/entitlements", api_premium.GetEntitlements) + premiumGroup.PUT("/active-guilds", api_premium.SetActiveGuilds) + } } guildAuthApiAdmin := apiGroup.Group("/:id", middleware.AuthenticateGuild(permission.Admin)) diff --git a/frontend/src/routes.js b/frontend/src/routes.js index e77e52e..0f8dac5 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -34,6 +34,7 @@ import CreatePanel from "./views/panels/CreatePanel.svelte"; import CreateMultiPanel from "./views/panels/CreateMultiPanel.svelte"; import EditPanel from "./views/panels/EditPanel.svelte"; import EditMultiPanel from "./views/panels/EditMultiPanel.svelte"; +import SelectServers from "./views/premium/SelectServers.svelte"; export const routes = [ {name: '/', component: Index, layout: IndexLayout}, @@ -43,6 +44,12 @@ export const routes = [ {name: '/logout', component: Logout}, {name: '/error', component: Error, layout: ErrorLayout}, {name: '/whitelabel', component: Whitelabel, layout: IndexLayout}, + { + name: 'premium', + nestedRoutes: [ + {name: 'select-servers', component: SelectServers, layout: IndexLayout} + ] + }, { name: 'admin', nestedRoutes: [ diff --git a/frontend/src/views/premium/SelectServers.svelte b/frontend/src/views/premium/SelectServers.svelte index e69de29..8c18418 100644 --- a/frontend/src/views/premium/SelectServers.svelte +++ b/frontend/src/views/premium/SelectServers.svelte @@ -0,0 +1,183 @@ +
+ + Choose Premium Servers +
+
+ Your premium subscription allows you to choose {@html limit === 1 ? "one server" : `up to ${limit} servers`} + to apply premium to. + + + Currently selected: {selected.length} / {limit} server{limit > 1 ? "s" : ""}. + +
+
+ {#each getAdminGuilds(guilds) as guild} +
toggleSelected(guild.id)}> + Guild Icon handleImgLoadError(e, guild.id)} /> + {guild.name} +
+ {/each} +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/go.mod b/go.mod index 2983658..619974d 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/BurntSushi/toml v1.2.1 github.com/TicketsBot/archiverclient v0.0.0-20240613013458-accc062facc2 github.com/TicketsBot/common v0.0.0-20240829163809-6f60869d8941 - github.com/TicketsBot/database v0.0.0-20240829163737-86b657b7e506 + github.com/TicketsBot/database v0.0.0-20240901155918-d0c56594a09a github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c github.com/TicketsBot/worker v0.0.0-20240829163848-84556c59ee72 github.com/apex/log v1.1.2 diff --git a/go.sum b/go.sum index c650bba..38ed2d2 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/TicketsBot/database v0.0.0-20240829155519-6b47874c9775 h1:4BhqGSlvI+t github.com/TicketsBot/database v0.0.0-20240829155519-6b47874c9775/go.mod h1:XhV3kI4ogTxBJATmO20kxmaynUcqCf3PCnbVbxkYNyQ= github.com/TicketsBot/database v0.0.0-20240829163737-86b657b7e506 h1:mU2wx9pyb7258RYyh9ZpTkDDFZMtP3C8YFphFEjxgWE= github.com/TicketsBot/database v0.0.0-20240829163737-86b657b7e506/go.mod h1:tnSNh3i0tPw2xmTzF8JnQPPpKY9QdwbeH2d4xzYcqcM= +github.com/TicketsBot/database v0.0.0-20240901155918-d0c56594a09a h1:kNDfpVimz3kEBYpiIql1rJDDUHiBKZEdw+JLyH4Ne9w= +github.com/TicketsBot/database v0.0.0-20240901155918-d0c56594a09a/go.mod h1:tnSNh3i0tPw2xmTzF8JnQPPpKY9QdwbeH2d4xzYcqcM= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c= github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=