wip: migrate to mono-repo. SPN has already been moved to spn/

This commit is contained in:
Patrick Pacher
2024-03-15 11:55:13 +01:00
parent b30fd00ccf
commit 8579430db9
577 changed files with 35981 additions and 818 deletions

View File

@@ -0,0 +1,65 @@
package account
import (
"errors"
"net/http"
)
// Authentication Headers.
const (
AuthHeaderDevice = "Device-17"
AuthHeaderToken = "Token-17"
AuthHeaderNextToken = "Next-Token-17"
AuthHeaderNextTokenDeprecated = "Next_token_17"
)
// Errors.
var (
ErrMissingDeviceID = errors.New("missing device ID")
ErrMissingToken = errors.New("missing token")
)
// AuthToken holds an authentication token.
type AuthToken struct {
Device string
Token string
}
// GetAuthTokenFromRequest extracts an authentication token from a request.
func GetAuthTokenFromRequest(request *http.Request) (*AuthToken, error) {
device := request.Header.Get(AuthHeaderDevice)
if device == "" {
return nil, ErrMissingDeviceID
}
token := request.Header.Get(AuthHeaderToken)
if token == "" {
return nil, ErrMissingToken
}
return &AuthToken{
Device: device,
Token: token,
}, nil
}
// ApplyTo applies the authentication token to a request.
func (at *AuthToken) ApplyTo(request *http.Request) {
request.Header.Set(AuthHeaderDevice, at.Device)
request.Header.Set(AuthHeaderToken, at.Token)
}
// GetNextTokenFromResponse extracts an authentication token from a response.
func GetNextTokenFromResponse(resp *http.Response) (token string, ok bool) {
token = resp.Header.Get(AuthHeaderNextToken)
if token == "" {
// TODO: Remove when fixed on server.
token = resp.Header.Get(AuthHeaderNextTokenDeprecated)
}
return token, token != ""
}
// ApplyNextTokenToResponse applies the next authentication token to a response.
func ApplyNextTokenToResponse(w http.ResponseWriter, token string) {
w.Header().Set(AuthHeaderNextToken, token)
}

View File

@@ -0,0 +1,14 @@
package account
// Customer Agent URLs.
const (
CAAuthenticateURL = "/authenticate"
CAProfileURL = "/user/profile"
CAGetTokensURL = "/tokens"
)
// Customer Hub URLs.
const (
CHAuthenticateURL = "/v1/authenticate"
CHUserProfileURL = "/v1/user_profile"
)

137
spn/access/account/types.go Normal file
View File

@@ -0,0 +1,137 @@
package account
import (
"time"
"golang.org/x/exp/slices"
)
// User, Subscription and Charge states.
const (
// UserStateNone is only used within Portmaster for saving information for
// logging into the same device.
UserStateNone = ""
UserStateFresh = "fresh"
UserStateQueued = "queued"
UserStateApproved = "approved"
UserStateSuspended = "suspended"
UserStateLoggedOut = "loggedout" // Portmaster only.
SubscriptionStateManual = "manual" // Manual renewal.
SubscriptionStateActive = "active" // Automatic renewal.
SubscriptionStateCancelled = "cancelled" // Automatic, but canceled.
ChargeStatePending = "pending"
ChargeStateCompleted = "completed"
ChargeStateDead = "dead"
)
// Agent and Hub return statuses.
const (
// StatusInvalidAuth [401 Unauthorized] is returned when the credentials are
// invalid or the user was logged out.
StatusInvalidAuth = 401
// StatusNoAccess [403 Forbidden] is returned when the user does not have
// an active subscription or the subscription does not include the required
// feature for the request.
StatusNoAccess = 403
// StatusInvalidDevice [410 Gone] is returned when the device trying to
// log into does not exist.
StatusInvalidDevice = 410
// StatusReachedDeviceLimit [409 Conflict] is returned when the device limit is reached.
StatusReachedDeviceLimit = 409
// StatusDeviceInactive [423 Locked] is returned when the device is locked.
StatusDeviceInactive = 423
// StatusNotLoggedIn [412 Precondition] is returned by the Portmaster, if an action required to be logged in, but the user is not logged in.
StatusNotLoggedIn = 412
// StatusUnknownError is a special status code that signifies an unknown or
// unexpected error by the API.
StatusUnknownError = -1
// StatusConnectionError is a special status code that signifies a
// connection error.
StatusConnectionError = -2
)
// User describes an SPN user account.
type User struct {
Username string `json:"username"`
State string `json:"state"`
Balance int `json:"balance"`
Device *Device `json:"device"`
Subscription *Subscription `json:"subscription"`
CurrentPlan *Plan `json:"current_plan"`
NextPlan *Plan `json:"next_plan"`
View *View `json:"view"`
}
// MayUseSPN returns whether the user may currently use the SPN.
func (u *User) MayUseSPN() bool {
return u.MayUse(FeatureSPN)
}
// MayUsePrioritySupport returns whether the user may currently use the priority support.
func (u *User) MayUsePrioritySupport() bool {
return u.MayUse(FeatureSafingSupport)
}
// MayUse returns whether the user may currently use the feature identified by
// the given feature ID.
// Leave feature ID empty to check without feature.
func (u *User) MayUse(featureID FeatureID) bool {
switch {
case u == nil:
// We need a user, obviously.
case u.State != UserStateApproved:
// Only approved users may use the SPN.
case u.Subscription == nil:
// Need a subscription.
case u.Subscription.EndsAt == nil:
case time.Now().After(*u.Subscription.EndsAt):
// Subscription needs to be active.
case u.CurrentPlan == nil:
// Need a plan / package.
case featureID != "" &&
!slices.Contains(u.CurrentPlan.FeatureIDs, featureID):
// Required feature ID must be in plan / package feature IDs.
default:
// All checks passed!
return true
}
return false
}
// Device describes a device of an SPN user.
type Device struct {
Name string `json:"name"`
ID string `json:"id"`
}
// Subscription describes an SPN subscription.
type Subscription struct {
EndsAt *time.Time `json:"ends_at"`
State string `json:"state"`
NextBillingDate *time.Time `json:"next_billing_date"`
PaymentProvider string `json:"payment_provider"`
}
// FeatureID defines a feature that requires a plan/subscription.
type FeatureID string
// A list of all supported features.
const (
FeatureSPN = FeatureID("spn")
FeatureSafingSupport = FeatureID("support")
FeatureHistory = FeatureID("history")
FeatureBWVis = FeatureID("bw-vis")
FeatureVPNCompat = FeatureID("vpn-compat")
)
// Plan describes an SPN subscription plan.
type Plan struct {
Name string `json:"name"`
Amount int `json:"amount"`
Months int `json:"months"`
Renewable bool `json:"renewable"`
FeatureIDs []FeatureID `json:"feature_ids"`
}

123
spn/access/account/view.go Normal file
View File

@@ -0,0 +1,123 @@
package account
import (
"fmt"
"strings"
"time"
)
// View holds metadata that assists in displaying account information.
type View struct {
Message string
ShowAccountData bool
ShowAccountButton bool
ShowLoginButton bool
ShowRefreshButton bool
ShowLogoutButton bool
}
// UpdateView updates the view and handles plan/package fallbacks.
func (u *User) UpdateView(requestStatusCode int) {
v := &View{}
// Clean up naming and fallbacks when finished.
defer func() {
// Display "Free" package if no plan is set or if it expired.
switch {
case u.CurrentPlan == nil,
u.Subscription == nil,
u.Subscription.EndsAt == nil:
// Reset to free plan.
u.CurrentPlan = &Plan{
Name: "Free",
}
u.Subscription = nil
case u.Subscription.NextBillingDate != nil:
// Subscription is on auto-renew.
// Wait for update from server.
case time.Since(*u.Subscription.EndsAt) > 0:
// Reset to free plan.
u.CurrentPlan = &Plan{
Name: "Free",
}
u.Subscription = nil
}
// Prepend "Portmaster " to plan name.
// TODO: Remove when Plan/Package naming has been updated.
if u.CurrentPlan != nil && !strings.HasPrefix(u.CurrentPlan.Name, "Portmaster ") {
u.CurrentPlan.Name = "Portmaster " + u.CurrentPlan.Name
}
// Apply new view to user.
u.View = v
}()
// Set view data based on return code.
switch requestStatusCode {
case StatusInvalidAuth, StatusInvalidDevice, StatusDeviceInactive:
// Account deleted or Device inactive or deleted.
// When using token based auth, there is no difference between these cases.
v.Message = "This device may have been deactivated or removed from your account. Please log in again."
v.ShowAccountData = true
v.ShowAccountButton = true
v.ShowLoginButton = true
v.ShowLogoutButton = true
return
case StatusUnknownError:
v.Message = "There is an unknown error in the communication with the account server. The shown information may not be accurate. "
case StatusConnectionError:
v.Message = "Portmaster could not connect to the account server. The shown information may not be accurate. "
}
// Set view data based on profile data.
switch {
case u.State == UserStateLoggedOut:
// User logged out.
v.ShowAccountButton = true
v.ShowLoginButton = true
return
case u.State == UserStateSuspended:
// Account is suspended.
v.Message += fmt.Sprintf("Your account (%s) was suspended. Please contact support for details.", u.Username)
v.ShowAccountButton = true
v.ShowRefreshButton = true
v.ShowLogoutButton = true
return
case u.Subscription == nil || u.Subscription.EndsAt == nil:
// Account has never had a subscription.
v.Message += "Get more features. Upgrade today."
case u.Subscription.NextBillingDate != nil:
switch {
case time.Since(*u.Subscription.NextBillingDate) > 0:
v.Message += "Your auto-renewal seems to be delayed. Please refresh and check the status of your payment. Payment information may be delayed."
case time.Until(*u.Subscription.NextBillingDate) < 24*time.Hour:
v.Message += "Your subscription will auto-renew soon. Please note that payment information may be delayed."
}
case time.Since(*u.Subscription.EndsAt) > 0:
// Subscription expired.
if u.CurrentPlan != nil {
v.Message += fmt.Sprintf("Your package %s has ended. Extend it on the Account Page.", u.CurrentPlan.Name)
} else {
v.Message += "Your package has ended. Extend it on the Account Page."
}
case time.Until(*u.Subscription.EndsAt) < 7*24*time.Hour:
// Add generic ending soon message if the package ends in less than 7 days.
v.Message += "Your package ends soon. Extend it on the Account Page."
}
// Defaults for generally good accounts.
v.ShowAccountData = true
v.ShowAccountButton = true
v.ShowRefreshButton = true
v.ShowLogoutButton = true
}