Skip to content

Commit 2914a95

Browse files
Do not treat expired license as an error
Also include expire_at in API response.
1 parent 96b8534 commit 2914a95

File tree

3 files changed

+691
-75
lines changed

3 files changed

+691
-75
lines changed

cmd/cli/main.go

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,12 @@ var jsonResponseLicenseKeyAlreadyActivated = map[string]any{
6969
},
7070
}
7171

72-
var jsonResponseLicenseKeyExpired = map[string]any{
73-
"error": map[string]any{
74-
"code": "license_key_expired",
75-
},
72+
func NewLicenseResponse(l *keygen.LicenseID) map[string]any {
73+
return map[string]any{
74+
"data": l,
75+
}
7676
}
7777

78-
var jsonResponseOK = map[string]any{}
79-
8078
var rootCmd = &cobra.Command{
8179
Use: "authgear-once-license-server",
8280
}
@@ -168,30 +166,27 @@ var serveCmd = &cobra.Command{
168166
return
169167
}
170168

171-
err = keygen.ActivateLicense(ctx, deps.HTTPClient, keygen.ActivateLicenseOptions{
169+
licenseID, err := keygen.ActivateLicense(ctx, deps.HTTPClient, keygen.ActivateLicenseOptions{
172170
KeygenConfig: deps.KeygenConfig,
173171
LicenseKey: licenseKey,
174172
Fingerprint: fingerprint,
175173
})
176174
if err != nil {
177175
switch {
178-
case errors.Is(err, keygen.ErrUnexpectedResponse):
179-
slogging.Error(ctx, logger, "unexpected keygen response",
180-
"error", err)
181-
WriteJSON(w, jsonResponseInternalServerError, http.StatusInternalServerError)
182-
return
183176
case errors.Is(err, keygen.ErrLicenseKeyNotFound):
184177
WriteJSON(w, jsonResponseLicenseKeyNotFound, http.StatusNotFound)
185178
return
186179
case errors.Is(err, keygen.ErrLicenseKeyAlreadyActivated):
187180
WriteJSON(w, jsonResponseLicenseKeyAlreadyActivated, http.StatusForbidden)
188181
return
189-
case errors.Is(err, keygen.ErrLicenseKeyExpired):
190-
WriteJSON(w, jsonResponseLicenseKeyExpired, http.StatusForbidden)
182+
default:
183+
slogging.Error(ctx, logger, "unexpected error",
184+
"error", err)
185+
WriteJSON(w, jsonResponseInternalServerError, http.StatusInternalServerError)
191186
return
192187
}
193188
}
194-
WriteJSON(w, jsonResponseOK, http.StatusOK)
189+
WriteJSON(w, NewLicenseResponse(licenseID), http.StatusOK)
195190
})
196191

197192
mux.HandleFunc("/v1/license/check", func(w http.ResponseWriter, r *http.Request) {
@@ -214,30 +209,27 @@ var serveCmd = &cobra.Command{
214209
return
215210
}
216211

217-
err = keygen.CheckLicense(ctx, deps.HTTPClient, keygen.CheckLicenseOptions{
212+
licenseID, err := keygen.CheckLicense(ctx, deps.HTTPClient, keygen.CheckLicenseOptions{
218213
KeygenConfig: deps.KeygenConfig,
219214
LicenseKey: licenseKey,
220215
Fingerprint: fingerprint,
221216
})
222217
if err != nil {
223218
switch {
224-
case errors.Is(err, keygen.ErrUnexpectedResponse):
225-
slogging.Error(ctx, logger, "unexpected keygen response",
226-
"error", err)
227-
WriteJSON(w, jsonResponseInternalServerError, http.StatusInternalServerError)
228-
return
229219
case errors.Is(err, keygen.ErrLicenseKeyNotFound):
230220
WriteJSON(w, jsonResponseLicenseKeyNotFound, http.StatusNotFound)
231221
return
232222
case errors.Is(err, keygen.ErrLicenseKeyAlreadyActivated):
233223
WriteJSON(w, jsonResponseLicenseKeyAlreadyActivated, http.StatusForbidden)
234224
return
235-
case errors.Is(err, keygen.ErrLicenseKeyExpired):
236-
WriteJSON(w, jsonResponseLicenseKeyExpired, http.StatusForbidden)
225+
default:
226+
slogging.Error(ctx, logger, "unexpected error",
227+
"error", err)
228+
WriteJSON(w, jsonResponseInternalServerError, http.StatusInternalServerError)
237229
return
238230
}
239231
}
240-
WriteJSON(w, jsonResponseOK, http.StatusOK)
232+
WriteJSON(w, NewLicenseResponse(licenseID), http.StatusOK)
241233
})
242234

243235
mux.HandleFunc("/v1/stripe/checkout", func(w http.ResponseWriter, r *http.Request) {

pkg/keygen/keygen.go

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import (
77
"encoding/json"
88
"errors"
99
"fmt"
10+
"io"
1011
"net/http"
1112
"net/http/httputil"
1213
"net/url"
14+
"time"
1315
)
1416

1517
var ErrUnexpectedResponse = errors.New("unexpected response")
1618
var ErrLicenseKeyNotFound = errors.New("license key not found")
1719
var ErrLicenseKeyAlreadyActivated = errors.New("license key already activated")
18-
var ErrLicenseKeyExpired = errors.New("license key expired")
1920

2021
type KeygenResponseError struct {
2122
DumpedResponse []byte
@@ -112,9 +113,12 @@ type validateLicenseKeyOptions struct {
112113
Fingerprint string
113114
}
114115

116+
// LicenseID is an API object.
115117
type LicenseID struct {
116-
ID string
117-
IsActivated bool
118+
ID string `json:"-"`
119+
ExpireAt *time.Time `json:"expire_at"`
120+
IsActivated bool `json:"is_activated"`
121+
IsExpired bool `json:"is_expired"`
118122
}
119123

120124
// validateLicenseKey returns the following errors:
@@ -164,48 +168,73 @@ func validateLicenseKey(ctx context.Context, client *http.Client, opts validateL
164168
}()
165169

166170
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
167-
var respBody map[string]any
168-
err = json.NewDecoder(resp.Body).Decode(&respBody)
169-
if err != nil {
170-
return
171-
}
171+
return parseValidateLicenseKeyResponseBody(resp.Body)
172+
}
172173

173-
meta := respBody["meta"].(map[string]any)
174-
meta_code := meta["code"].(string)
174+
err = ErrUnexpectedResponse
175+
return
176+
}
175177

176-
if meta_code == "NOT_FOUND" {
177-
err = ErrLicenseKeyNotFound
178-
return
179-
} else {
180-
status := respBody["data"].(map[string]any)["attributes"].(map[string]any)["status"].(string)
181-
if status == "EXPIRED" {
182-
err = ErrLicenseKeyExpired
183-
return
184-
}
178+
func parseValidateLicenseKeyResponseBody(r io.Reader) (licenseID *LicenseID, err error) {
179+
var respBody map[string]any
180+
err = json.NewDecoder(r).Decode(&respBody)
181+
if err != nil {
182+
return
183+
}
185184

186-
switch meta_code {
187-
case "FINGERPRINT_SCOPE_MISMATCH":
188-
err = ErrLicenseKeyAlreadyActivated
189-
return
190-
case "EXPIRED":
191-
err = ErrLicenseKeyExpired
192-
return
193-
case "NO_MACHINE", "VALID":
194-
data, ok := respBody["data"].(map[string]any)
195-
if !ok || data == nil {
196-
err = ErrUnexpectedResponse
197-
return
198-
}
199-
licenseID = &LicenseID{
200-
ID: data["id"].(string),
201-
IsActivated: meta_code == "VALID",
202-
}
203-
return
204-
}
185+
meta := respBody["meta"].(map[string]any)
186+
meta_code := meta["code"].(string)
187+
188+
if meta_code == "NOT_FOUND" {
189+
err = ErrLicenseKeyNotFound
190+
return
191+
}
192+
193+
if meta_code == "FINGERPRINT_SCOPE_MISMATCH" {
194+
err = ErrLicenseKeyAlreadyActivated
195+
return
196+
}
197+
198+
data, ok := respBody["data"].(map[string]any)
199+
if !ok || data == nil {
200+
err = ErrUnexpectedResponse
201+
return
202+
}
203+
attributes, ok := data["attributes"].(map[string]any)
204+
if !ok {
205+
err = ErrUnexpectedResponse
206+
return
207+
}
208+
209+
licenseID = &LicenseID{
210+
ID: data["id"].(string),
211+
}
212+
213+
if expiry, ok := attributes["expiry"].(string); ok {
214+
var expireAt time.Time
215+
expireAt, err = time.Parse(time.RFC3339, expiry)
216+
if err != nil {
217+
err = errors.Join(err, ErrUnexpectedResponse)
218+
return
205219
}
220+
licenseID.ExpireAt = &expireAt
221+
}
222+
223+
switch meta_code {
224+
case "VALID":
225+
licenseID.IsActivated = true
226+
licenseID.IsExpired = false
227+
case "EXPIRED":
228+
licenseID.IsActivated = true
229+
licenseID.IsExpired = true
230+
case "NO_MACHINE":
231+
licenseID.IsActivated = false
232+
licenseID.IsExpired = false
233+
default:
234+
err = ErrUnexpectedResponse
235+
return
206236
}
207237

208-
err = ErrUnexpectedResponse
209238
return
210239
}
211240

@@ -421,10 +450,10 @@ type ActivateLicenseOptions struct {
421450
// - ErrUnexpectedResponse
422451
// - ErrLicenseKeyNotFound
423452
// - ErrLicenseKeyAlreadyActivated
424-
func ActivateLicense(ctx context.Context, client *http.Client, opts ActivateLicenseOptions) (err error) {
453+
func ActivateLicense(ctx context.Context, client *http.Client, opts ActivateLicenseOptions) (licenseID *LicenseID, err error) {
425454
// We first try to validate the license key.
426455
// If the license is activated, we can return early.
427-
licenseID, err := validateLicenseKey(ctx, client, validateLicenseKeyOptions{
456+
licenseID, err = validateLicenseKey(ctx, client, validateLicenseKeyOptions{
428457
KeygenConfig: opts.KeygenConfig,
429458
LicenseKey: opts.LicenseKey,
430459
Fingerprint: opts.Fingerprint,
@@ -475,20 +504,12 @@ type CheckLicenseOptions struct {
475504
// - ErrUnexpectedResponse
476505
// - ErrLicenseKeyNotFound
477506
// - ErrLicenseKeyAlreadyActivated
478-
func CheckLicense(ctx context.Context, client *http.Client, opts CheckLicenseOptions) (err error) {
479-
licenseID, err := validateLicenseKey(ctx, client, validateLicenseKeyOptions{
507+
func CheckLicense(ctx context.Context, client *http.Client, opts CheckLicenseOptions) (licenseID *LicenseID, err error) {
508+
licenseID, err = validateLicenseKey(ctx, client, validateLicenseKeyOptions{
480509
KeygenConfig: opts.KeygenConfig,
481510
LicenseKey: opts.LicenseKey,
482511
Fingerprint: opts.Fingerprint,
483512
})
484-
if err != nil {
485-
return
486-
}
487-
// Activate the same fingerprint is idempotent.
488-
if licenseID.IsActivated {
489-
return
490-
}
491-
492513
return
493514
}
494515

0 commit comments

Comments
 (0)