wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
65
spn/access/account/auth.go
Normal file
65
spn/access/account/auth.go
Normal 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)
|
||||
}
|
||||
14
spn/access/account/client.go
Normal file
14
spn/access/account/client.go
Normal 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
137
spn/access/account/types.go
Normal 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
123
spn/access/account/view.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user