Skip to content

Commit 468befb

Browse files
committed
in app purchases backend -- apple
1 parent 0ff2d46 commit 468befb

File tree

2 files changed

+296
-15
lines changed

2 files changed

+296
-15
lines changed

api/v1/subscription/subscription.go

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package subscription
22

33
import (
4+
"bytes"
45
"crypto/hmac"
56
"crypto/sha256"
67
"encoding/json"
8+
"fmt"
79
"io"
810
"net/http"
911
"os"
@@ -26,13 +28,282 @@ func ApplyRoutes(r *gin.RouterGroup) {
2628

2729
g.POST("/helio", createHelioSubscription)
2830
g.POST("", createSubscription)
31+
g.POST("/apple", createSubscriptionApple)
2932
g.GET("/:id", readSubscription)
3033
g.PATCH("/:id", updateSubscription)
3134
g.DELETE("/:id", deleteSubscription)
3235
g.GET("", readSubscriptions)
3336
}
3437
}
3538

39+
func createSubscriptionApple(c *gin.Context) {
40+
var receipt model.PurchaseRceipt
41+
42+
err := json.NewDecoder(c.Request.Body).Decode(&receipt)
43+
if err != nil {
44+
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
45+
return
46+
}
47+
48+
// Validate the receipt with Apple
49+
valid, err := validateReceiptApple(receipt.Receipt)
50+
if err != nil || !valid {
51+
c.JSON(http.StatusForbidden, gin.H{"error": "Invalid receipt"})
52+
return
53+
}
54+
55+
customer_name := receipt.Name
56+
57+
credits := 0
58+
name := ""
59+
description := ""
60+
devices := 0
61+
networks := 0
62+
members := 0
63+
relays := 0
64+
autoRenew := false
65+
issued := time.Now()
66+
expires := time.Now().AddDate(1, 0, 0)
67+
68+
// set the credits, name, and description based on the sku
69+
switch receipt.ProductID {
70+
case "24_hours":
71+
credits = 1
72+
name = "24 Hours"
73+
description = "Service in any region for 24 hours"
74+
devices = 5
75+
networks = 1
76+
members = 2
77+
relays = 1
78+
autoRenew = false
79+
expires = time.Now().Add(24 * time.Hour)
80+
case "1_month":
81+
credits = 1
82+
name = "1 Month"
83+
description = "Service in any region for 1 month"
84+
devices = 5
85+
networks = 1
86+
members = 2
87+
relays = 1
88+
autoRenew = false
89+
expires = time.Now().AddDate(0, 1, 0)
90+
case "1_week":
91+
credits = 1
92+
name = "1 Week"
93+
description = "Service in any region for 1 week"
94+
devices = 5
95+
networks = 1
96+
members = 2
97+
relays = 1
98+
autoRenew = false
99+
expires = time.Now().AddDate(0, 0, 7)
100+
case "basic_monthly":
101+
fallthrough
102+
case "basic_yearly":
103+
credits = 1
104+
name = "Basic Service"
105+
description = "A single tunnel or relay in any region"
106+
devices = 5
107+
networks = 1
108+
members = 2
109+
relays = 1
110+
autoRenew = true
111+
case "premium_monthly":
112+
fallthrough
113+
case "premium_yearly":
114+
credits = 5
115+
name = "Premium"
116+
description = "Up to 5 tunnels or relays in any region"
117+
devices = 25
118+
networks = 10
119+
members = 5
120+
relays = 5
121+
autoRenew = true
122+
case "professional_monthly":
123+
fallthrough
124+
case "professional_yearly":
125+
credits = 10
126+
name = "Professional"
127+
description = "Up to 10 tunnels or relays in any region"
128+
devices = 100
129+
networks = 25
130+
members = 25
131+
relays = 10
132+
autoRenew = true
133+
default:
134+
log.Errorf("unknown sku %s", receipt.ProductID)
135+
}
136+
137+
// set the limits based on the sku
138+
accounts, err := core.ReadAllAccounts(receipt.Email)
139+
if err != nil {
140+
log.Error(err)
141+
} else {
142+
// If there's no error and no account, create one.
143+
if len(accounts) == 0 {
144+
var account model.Account
145+
account.Name = customer_name
146+
account.AccountName = "Company"
147+
account.Email = receipt.Email
148+
account.Role = "Owner"
149+
account.Status = "Active"
150+
account.CreatedBy = receipt.Email
151+
account.UpdatedBy = receipt.Email
152+
account.Picture = "/account-circle.svg"
153+
154+
a, err := core.CreateAccount(&account)
155+
log.Infof("CREATE ACCOUNT = %v", a)
156+
if err != nil {
157+
log.Error(err)
158+
}
159+
accounts, err = core.ReadAllAccounts(receipt.Email)
160+
if err != nil {
161+
log.Error(err)
162+
}
163+
164+
}
165+
}
166+
167+
var account *model.Account
168+
for i := 0; i < len(accounts); i++ {
169+
if accounts[i].Id == accounts[i].Parent {
170+
account = accounts[i]
171+
break
172+
}
173+
}
174+
175+
if account == nil {
176+
log.Errorf("account not found for email %s", receipt.Email)
177+
c.JSON(http.StatusNotFound, gin.H{"error": "Account not found"})
178+
return
179+
}
180+
181+
limits, err := core.ReadLimits(account.Id)
182+
if err != nil {
183+
log.Error(err)
184+
limits_id, err := util.GenerateRandomString(8)
185+
if err != nil {
186+
log.Error(err)
187+
}
188+
limits_id = "limits-" + limits_id
189+
190+
limits = &model.Limits{
191+
Id: limits_id,
192+
AccountID: account.Id,
193+
MaxDevices: 0,
194+
MaxNetworks: 0,
195+
MaxMembers: 0,
196+
MaxServices: 0,
197+
Tolerance: core.GetDefaultTolerance(),
198+
CreatedBy: receipt.Email,
199+
UpdatedBy: receipt.Email,
200+
Created: time.Now(),
201+
Updated: time.Now(),
202+
}
203+
}
204+
205+
limits.MaxDevices += devices
206+
limits.MaxNetworks += networks
207+
limits.MaxMembers += members
208+
limits.MaxServices += relays
209+
210+
errs := limits.IsValid()
211+
if len(errs) != 0 {
212+
for _, err := range errs {
213+
log.WithFields(log.Fields{
214+
"err": err,
215+
}).Error("limits validation error")
216+
}
217+
c.JSON(http.StatusBadRequest, gin.H{"error": "Limits validation error"})
218+
return
219+
}
220+
221+
// save limits to mongodb
222+
mongo.Serialize(limits.Id, "id", "limits", limits)
223+
224+
// generate a random subscription id
225+
id, err := util.RandomString(8)
226+
if err != nil {
227+
log.Error(err)
228+
}
229+
id = receipt.Source + "-" + id
230+
231+
// construct a subscription object
232+
lu := time.Now()
233+
subscription := model.Subscription{
234+
Id: id,
235+
AccountID: account.Id,
236+
Email: receipt.Email,
237+
Name: name,
238+
Description: description,
239+
Issued: &issued,
240+
LastUpdated: &lu,
241+
Expires: &expires,
242+
Credits: credits,
243+
Sku: receipt.ProductID,
244+
Status: "active",
245+
AutoRenew: autoRenew,
246+
Receipt: receipt.Receipt,
247+
}
248+
249+
errs = subscription.IsValid()
250+
if len(errs) != 0 {
251+
for _, err := range errs {
252+
log.WithFields(log.Fields{
253+
"err": err,
254+
}).Error("subscription validation error")
255+
}
256+
return
257+
}
258+
259+
// save subscription to mongodb
260+
mongo.Serialize(subscription.Id, "id", "subscriptions", subscription)
261+
262+
c.JSON(http.StatusOK, subscription)
263+
return
264+
265+
}
266+
267+
func validateReceiptApple(receipt string) (bool, error) {
268+
// Apple receipt validation URL
269+
url := "https://buy.itunes.apple.com/verifyReceipt"
270+
271+
// Create the request payload
272+
payload := map[string]string{
273+
"receipt-data": receipt,
274+
"password": os.Getenv("APPLE_ITUNES_SHARED_SECRET"), // Replace with your app's shared secret
275+
}
276+
payloadBytes, err := json.Marshal(payload)
277+
if err != nil {
278+
return false, err
279+
}
280+
281+
// Send the request to Apple
282+
resp, err := http.Post(url, "application/json", bytes.NewBuffer(payloadBytes))
283+
if err != nil {
284+
return false, err
285+
}
286+
defer resp.Body.Close()
287+
288+
if resp.StatusCode != http.StatusOK {
289+
return false, fmt.Errorf("invalid response from Apple: %s", resp.Status)
290+
}
291+
292+
// Parse the response to check the receipt status
293+
var result map[string]interface{}
294+
err = json.NewDecoder(resp.Body).Decode(&result)
295+
if err != nil {
296+
return false, err
297+
}
298+
299+
// Check if the receipt is valid
300+
if status, ok := result["status"].(float64); ok && status == 0 {
301+
return true, nil
302+
}
303+
304+
return false, nil
305+
}
306+
36307
func createHelioSubscription(c *gin.Context) {
37308
var body string
38309
var sub map[string]interface{}

model/subscription.go

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,32 @@ import (
55
"time"
66
)
77

8-
// Net structure
8+
type PurchaseRceipt struct {
9+
AccountID string `json:"accountid" bson:"accountid"`
10+
Email string `json:"email" bson:"email"`
11+
Name string `json:"name" bson:"name"`
12+
Source string `json:"source" bson:"source"`
13+
ProductID string `json:"productid" bson:"productid"`
14+
Receipt string `json:"receipt" bson:"receipt"`
15+
}
16+
17+
// Subscription structure
918
type Subscription struct {
10-
Id string `json:"id" bson:"id"`
11-
AccountID string `json:"accountid" bson:"accountid"`
12-
Email string `json:"email" bson:"email"`
13-
Name string `json:"name" bson:"name"`
14-
Description string `json:"description" bson:"description"`
15-
Issued *time.Time `json:"issued" bson:"issued"`
16-
Expires *time.Time `json:"expires" bson:"expires"`
17-
LastUpdated *time.Time `json:"lastUpdated" bson:"lastUpdated"`
18-
CreatedBy string `json:"createdBy" bson:"createdBy"`
19-
UpdatedBy string `json:"updatedBy" bson:"updatedBy"`
20-
Status string `json:"status" bson:"status"`
21-
Sku string `json:"sku" bson:"sku"`
22-
Credits int `json:"credits" bson:"credits"`
23-
AutoRenew bool `json:"autoRenew" bson:"autoRenew"`
19+
Id string `json:"id" bson:"id"`
20+
AccountID string `json:"accountid" bson:"accountid"`
21+
Email string `json:"email" bson:"email"`
22+
Name string `json:"name" bson:"name"`
23+
Description string `json:"description" bson:"description"`
24+
Issued *time.Time `json:"issued" bson:"issued"`
25+
Expires *time.Time `json:"expires" bson:"expires"`
26+
LastUpdated *time.Time `json:"lastUpdated" bson:"lastUpdated"`
27+
CreatedBy string `json:"createdBy" bson:"createdBy"`
28+
UpdatedBy string `json:"updatedBy" bson:"updatedBy"`
29+
Status string `json:"status" bson:"status"`
30+
Sku string `json:"sku" bson:"sku"`
31+
Credits int `json:"credits" bson:"credits"`
32+
AutoRenew bool `json:"autoRenew" bson:"autoRenew"`
33+
Receipt string `json:"receipt,omitempty" bson:"receipt,omitempty"`
2434
}
2535

2636
// IsValid check if model is valid

0 commit comments

Comments
 (0)