satellite/console/consoleweb: remove templating for index.html
Previously, we evaluated index.html as a template in order to insert frontend config values into meta tags. Now that the frontend fetches its config through the satellite API, this is no longer necessary. Resolves #5494 Change-Id: Ic98507c5e16cd80317bd9c31d4b55abda0dd7e34
This commit is contained in:
parent
6e866856c4
commit
45d5a93085
@ -10,6 +10,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
"mime"
|
"mime"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -30,7 +31,6 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"storj.io/common/errs2"
|
"storj.io/common/errs2"
|
||||||
"storj.io/common/memory"
|
|
||||||
"storj.io/common/storj"
|
"storj.io/common/storj"
|
||||||
"storj.io/storj/private/web"
|
"storj.io/storj/private/web"
|
||||||
"storj.io/storj/satellite/abtesting"
|
"storj.io/storj/satellite/abtesting"
|
||||||
@ -134,12 +134,7 @@ type Server struct {
|
|||||||
|
|
||||||
schema graphql.Schema
|
schema graphql.Schema
|
||||||
|
|
||||||
templatesCache *templates
|
errorTemplate *template.Template
|
||||||
}
|
|
||||||
|
|
||||||
type templates struct {
|
|
||||||
index *template.Template
|
|
||||||
error *template.Template
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiAuth exposes methods to control authentication process for each generated API endpoint.
|
// apiAuth exposes methods to control authentication process for each generated API endpoint.
|
||||||
@ -382,9 +377,9 @@ func (server *Server) Run(ctx context.Context) (err error) {
|
|||||||
return Error.Wrap(err)
|
return Error.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = server.loadTemplates()
|
_, err = server.loadErrorTemplate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: should it return error if some template can not be initialized or just log about it?
|
// TODO: should it return error if the template cannot be initialized or just log about it?
|
||||||
return Error.Wrap(err)
|
return Error.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,99 +434,32 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
header.Set("X-Content-Type-Options", "nosniff")
|
header.Set("X-Content-Type-Options", "nosniff")
|
||||||
header.Set("Referrer-Policy", "same-origin") // Only expose the referring url when navigating around the satellite itself.
|
header.Set("Referrer-Policy", "same-origin") // Only expose the referring url when navigating around the satellite itself.
|
||||||
|
|
||||||
var data struct {
|
path := filepath.Join(server.config.StaticDir, "dist", "index.html")
|
||||||
ExternalAddress string
|
file, err := os.Open(path)
|
||||||
SatelliteName string
|
if err != nil {
|
||||||
SatelliteNodeURL string
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
StripePublicKey string
|
server.log.Error("index.html was not generated. run 'npm run build' in the "+server.config.StaticDir+" directory", zap.Error(err))
|
||||||
PartneredSatellites string
|
} else {
|
||||||
DefaultProjectLimit int
|
server.log.Error("error loading index.html", zap.String("path", path), zap.Error(err))
|
||||||
GeneralRequestURL string
|
}
|
||||||
ProjectLimitsIncreaseRequestURL string
|
server.serveError(w, http.StatusInternalServerError)
|
||||||
GatewayCredentialsRequestURL string
|
|
||||||
IsBetaSatellite bool
|
|
||||||
BetaSatelliteFeedbackURL string
|
|
||||||
BetaSatelliteSupportURL string
|
|
||||||
DocumentationURL string
|
|
||||||
CouponCodeBillingUIEnabled bool
|
|
||||||
CouponCodeSignupUIEnabled bool
|
|
||||||
FileBrowserFlowDisabled bool
|
|
||||||
LinksharingURL string
|
|
||||||
PathwayOverviewEnabled bool
|
|
||||||
RegistrationRecaptchaEnabled bool
|
|
||||||
RegistrationRecaptchaSiteKey string
|
|
||||||
RegistrationHcaptchaEnabled bool
|
|
||||||
RegistrationHcaptchaSiteKey string
|
|
||||||
LoginRecaptchaEnabled bool
|
|
||||||
LoginRecaptchaSiteKey string
|
|
||||||
LoginHcaptchaEnabled bool
|
|
||||||
LoginHcaptchaSiteKey string
|
|
||||||
AllProjectsDashboard bool
|
|
||||||
DefaultPaidStorageLimit memory.Size
|
|
||||||
DefaultPaidBandwidthLimit memory.Size
|
|
||||||
InactivityTimerEnabled bool
|
|
||||||
InactivityTimerDuration int
|
|
||||||
InactivityTimerViewerEnabled bool
|
|
||||||
OptionalSignupSuccessURL string
|
|
||||||
HomepageURL string
|
|
||||||
NativeTokenPaymentsEnabled bool
|
|
||||||
PasswordMinimumLength int
|
|
||||||
PasswordMaximumLength int
|
|
||||||
ABTestingEnabled bool
|
|
||||||
PricingPackagesEnabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
data.ExternalAddress = server.config.ExternalAddress
|
|
||||||
data.SatelliteName = server.config.SatelliteName
|
|
||||||
data.SatelliteNodeURL = server.nodeURL.String()
|
|
||||||
data.StripePublicKey = server.stripePublicKey
|
|
||||||
data.PartneredSatellites = server.config.PartneredSatellites.String()
|
|
||||||
data.DefaultProjectLimit = server.config.DefaultProjectLimit
|
|
||||||
data.GeneralRequestURL = server.config.GeneralRequestURL
|
|
||||||
data.ProjectLimitsIncreaseRequestURL = server.config.ProjectLimitsIncreaseRequestURL
|
|
||||||
data.GatewayCredentialsRequestURL = server.config.GatewayCredentialsRequestURL
|
|
||||||
data.IsBetaSatellite = server.config.IsBetaSatellite
|
|
||||||
data.BetaSatelliteFeedbackURL = server.config.BetaSatelliteFeedbackURL
|
|
||||||
data.BetaSatelliteSupportURL = server.config.BetaSatelliteSupportURL
|
|
||||||
data.DocumentationURL = server.config.DocumentationURL
|
|
||||||
data.CouponCodeBillingUIEnabled = server.config.CouponCodeBillingUIEnabled
|
|
||||||
data.CouponCodeSignupUIEnabled = server.config.CouponCodeSignupUIEnabled
|
|
||||||
data.FileBrowserFlowDisabled = server.config.FileBrowserFlowDisabled
|
|
||||||
data.LinksharingURL = server.config.LinksharingURL
|
|
||||||
data.PathwayOverviewEnabled = server.config.PathwayOverviewEnabled
|
|
||||||
data.DefaultPaidStorageLimit = server.config.UsageLimits.Storage.Paid
|
|
||||||
data.DefaultPaidBandwidthLimit = server.config.UsageLimits.Bandwidth.Paid
|
|
||||||
data.RegistrationRecaptchaEnabled = server.config.Captcha.Registration.Recaptcha.Enabled
|
|
||||||
data.RegistrationRecaptchaSiteKey = server.config.Captcha.Registration.Recaptcha.SiteKey
|
|
||||||
data.RegistrationHcaptchaEnabled = server.config.Captcha.Registration.Hcaptcha.Enabled
|
|
||||||
data.RegistrationHcaptchaSiteKey = server.config.Captcha.Registration.Hcaptcha.SiteKey
|
|
||||||
data.LoginRecaptchaEnabled = server.config.Captcha.Login.Recaptcha.Enabled
|
|
||||||
data.LoginRecaptchaSiteKey = server.config.Captcha.Login.Recaptcha.SiteKey
|
|
||||||
data.LoginHcaptchaEnabled = server.config.Captcha.Login.Hcaptcha.Enabled
|
|
||||||
data.LoginHcaptchaSiteKey = server.config.Captcha.Login.Hcaptcha.SiteKey
|
|
||||||
data.AllProjectsDashboard = server.config.AllProjectsDashboard
|
|
||||||
data.InactivityTimerEnabled = server.config.Session.InactivityTimerEnabled
|
|
||||||
data.InactivityTimerDuration = server.config.Session.InactivityTimerDuration
|
|
||||||
data.InactivityTimerViewerEnabled = server.config.Session.InactivityTimerViewerEnabled
|
|
||||||
data.OptionalSignupSuccessURL = server.config.OptionalSignupSuccessURL
|
|
||||||
data.HomepageURL = server.config.HomepageURL
|
|
||||||
data.NativeTokenPaymentsEnabled = server.config.NativeTokenPaymentsEnabled
|
|
||||||
data.PasswordMinimumLength = console.PasswordMinimumLength
|
|
||||||
data.PasswordMaximumLength = console.PasswordMaximumLength
|
|
||||||
data.ABTestingEnabled = server.config.ABTesting.Enabled
|
|
||||||
data.PricingPackagesEnabled = server.config.PricingPackagesEnabled
|
|
||||||
|
|
||||||
templates, err := server.loadTemplates()
|
|
||||||
if err != nil || templates.index == nil {
|
|
||||||
server.log.Error("unable to load templates", zap.Error(err))
|
|
||||||
fmt.Fprintf(w, "Unable to load templates. See whether satellite UI has been built.")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := templates.index.Execute(w, data); err != nil {
|
defer func() {
|
||||||
server.log.Error("index template could not be executed", zap.Error(err))
|
if err := file.Close(); err != nil {
|
||||||
|
server.log.Error("error closing index.html", zap.String("path", path), zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
server.log.Error("failed to retrieve file info", zap.Error(err))
|
||||||
|
server.serveError(w, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http.ServeContent(w, r, path, info.ModTime(), file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// withAuth performs initial authorization before every request.
|
// withAuth performs initial authorization before every request.
|
||||||
@ -854,18 +782,18 @@ func (server *Server) graphqlHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
server.log.Debug(fmt.Sprintf("%s", result))
|
server.log.Debug(fmt.Sprintf("%s", result))
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveError serves error static pages.
|
// serveError serves a static error page.
|
||||||
func (server *Server) serveError(w http.ResponseWriter, status int) {
|
func (server *Server) serveError(w http.ResponseWriter, status int) {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
|
|
||||||
templates, err := server.loadTemplates()
|
template, err := server.loadErrorTemplate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.log.Error("unable to load templates", zap.Error(err))
|
server.log.Error("unable to load error template", zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := struct{ StatusCode int }{StatusCode: status}
|
data := struct{ StatusCode int }{StatusCode: status}
|
||||||
err = templates.error.Execute(w, data)
|
err = template.Execute(w, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.log.Error("cannot parse error template", zap.Error(err))
|
server.log.Error("cannot parse error template", zap.Error(err))
|
||||||
}
|
}
|
||||||
@ -916,40 +844,16 @@ func (server *Server) brotliMiddleware(fn http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadTemplates is used to initialize all templates.
|
// loadTemplates is used to initialize the error page template.
|
||||||
func (server *Server) loadTemplates() (_ *templates, err error) {
|
func (server *Server) loadErrorTemplate() (_ *template.Template, err error) {
|
||||||
if server.config.Watch {
|
if server.errorTemplate == nil || server.config.Watch {
|
||||||
return server.parseTemplates()
|
server.errorTemplate, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "errors", "error.html"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, Error.Wrap(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if server.templatesCache != nil {
|
return server.errorTemplate, nil
|
||||||
return server.templatesCache, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
templates, err := server.parseTemplates()
|
|
||||||
if err != nil {
|
|
||||||
return nil, Error.Wrap(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server.templatesCache = templates
|
|
||||||
return server.templatesCache, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) parseTemplates() (_ *templates, err error) {
|
|
||||||
var t templates
|
|
||||||
|
|
||||||
t.index, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "dist", "index.html"))
|
|
||||||
if err != nil {
|
|
||||||
server.log.Error("dist folder is not generated. use 'npm run build' command", zap.Error(err))
|
|
||||||
// Loading index is optional.
|
|
||||||
}
|
|
||||||
|
|
||||||
t.error, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "errors", "error.html"))
|
|
||||||
if err != nil {
|
|
||||||
return &t, Error.Wrap(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &t, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserIDRateLimiter constructs a RateLimiter that limits based on user ID.
|
// NewUserIDRateLimiter constructs a RateLimiter that limits based on user ID.
|
||||||
|
@ -3,45 +3,6 @@
|
|||||||
<head lang="en">
|
<head lang="en">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="external-address" content="{{ .ExternalAddress }}">
|
|
||||||
<meta name="satellite-name" content="{{ .SatelliteName }}">
|
|
||||||
<meta name="satellite-nodeurl" content="{{ .SatelliteNodeURL }}">
|
|
||||||
<meta name="stripe-public-key" content="{{ .StripePublicKey }}">
|
|
||||||
<meta name="partnered-satellites" content="{{ .PartneredSatellites }}">
|
|
||||||
<meta name="default-project-limit" content="{{ .DefaultProjectLimit }}">
|
|
||||||
<meta name="general-request-url" content="{{ .GeneralRequestURL }}">
|
|
||||||
<meta name="project-limits-increase-request-url" content="{{ .ProjectLimitsIncreaseRequestURL }}">
|
|
||||||
<meta name="gateway-credentials-request-url" content="{{ .GatewayCredentialsRequestURL }}">
|
|
||||||
<meta name="is-beta-satellite" content="{{ .IsBetaSatellite }}">
|
|
||||||
<meta name="beta-satellite-feedback-url" content="{{ .BetaSatelliteFeedbackURL }}">
|
|
||||||
<meta name="beta-satellite-support-url" content="{{ .BetaSatelliteSupportURL }}">
|
|
||||||
<meta name="documentation-url" content="{{ .DocumentationURL }}">
|
|
||||||
<meta name="coupon-code-billing-ui-enabled" content="{{ .CouponCodeBillingUIEnabled }}">
|
|
||||||
<meta name="coupon-code-signup-ui-enabled" content="{{ .CouponCodeSignupUIEnabled }}">
|
|
||||||
<meta name="file-browser-flow-disabled" content="{{ .FileBrowserFlowDisabled }}">
|
|
||||||
<meta name="linksharing-url" content="{{ .LinksharingURL }}">
|
|
||||||
<meta name="registration-recaptcha-enabled" content="{{ .RegistrationRecaptchaEnabled }}">
|
|
||||||
<meta name="registration-recaptcha-site-key" content="{{ .RegistrationRecaptchaSiteKey }}">
|
|
||||||
<meta name="registration-hcaptcha-enabled" content="{{ .RegistrationHcaptchaEnabled }}">
|
|
||||||
<meta name="registration-hcaptcha-site-key" content="{{ .RegistrationHcaptchaSiteKey }}">
|
|
||||||
<meta name="login-recaptcha-enabled" content="{{ .LoginRecaptchaEnabled }}">
|
|
||||||
<meta name="login-recaptcha-site-key" content="{{ .LoginRecaptchaSiteKey }}">
|
|
||||||
<meta name="login-hcaptcha-enabled" content="{{ .LoginHcaptchaEnabled }}">
|
|
||||||
<meta name="login-hcaptcha-site-key" content="{{ .LoginHcaptchaSiteKey }}">
|
|
||||||
<meta name="all-projects-dashboard" content="{{ .AllProjectsDashboard }}">
|
|
||||||
<meta name="default-paid-storage-limit" content="{{ .DefaultPaidStorageLimit }}">
|
|
||||||
<meta name="default-paid-bandwidth-limit" content="{{ .DefaultPaidBandwidthLimit }}">
|
|
||||||
<meta name="inactivity-timer-enabled" content="{{ .InactivityTimerEnabled }}">
|
|
||||||
<meta name="inactivity-timer-duration" content="{{ .InactivityTimerDuration }}">
|
|
||||||
<meta name="inactivity-timer-viewer-enabled" content="{{ .InactivityTimerViewerEnabled }}">
|
|
||||||
<meta name="optional-signup-success-url" content="{{ .OptionalSignupSuccessURL }}">
|
|
||||||
<meta name="homepage-url" content="{{ .HomepageURL }}">
|
|
||||||
<meta name="native-token-payments-enabled" content="{{ .NativeTokenPaymentsEnabled }}">
|
|
||||||
<meta name="password-minimum-length" content="{{ .PasswordMinimumLength }}">
|
|
||||||
<meta name="password-maximum-length" content="{{ .PasswordMaximumLength }}">
|
|
||||||
<meta name="ab-testing-enabled" content="{{ .ABTestingEnabled }}">
|
|
||||||
<meta name="pricing-packages-enabled" content="{{ .PricingPackagesEnabled }}">
|
|
||||||
<title>{{ .SatelliteName }}</title>
|
|
||||||
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACDVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////8nbP8obf8pbf8qbv8rb/8sb/8tcP8ucf8vcf8vcv8xc/8zdP81df81dv82dv83d/84eP85eP86ef87ev89e/8+fP8/fP9Aff9Bfv9Cfv9Df/9EgP9FgP9Ggf9Hgv9Jg/9LhP9Mhf9Nhv9Oh/9Ph/9RiP9Rif9Sif9Ui/9Vi/9WjP9Xjf9Yjf9aj/9dkf9ekf9ilP9jlf9llv9nl/9omP9rmv9sm/9tnP9vnf9wnv9yn/91of92ov93o/94o/98pv9/qP+Bqf+Cqv+Eq/+FrP+Hrf+Irv+Jr/+Kr/+Msf+Nsv+Stf+Ttf+Ttv+Utv+Vt/+WuP+XuP+Zuf+Zuv+hv/+kwf+lwv+mwv+nw/+oxP+pxP+pxf+qxf+rxv+yy/+0zP+1zf+3zv+4z/+60P+70f+90v+/0/+/1P/B1f/D1v/E1//F1//F2P/G2P/H2f/I2v/J2v/K2//L3P/P3v/Q3//R4P/S4P/V4v/V4//W4//X5P/Y5P/b5v/b5//c5//d6P/e6f/f6f/g6v/h6//j7P/k7f/l7f/m7v/q8f/r8f/u8//w9f/x9f/x9v/z9//0+P/2+f/3+f/3+v/4+v/5+//6/P/8/f/9/v/+/v////9uCbVDAAAAFXRSTlMABAU4Ozw9PpSWl5ilp6ip4+Tl/P6nIcp/AAAAAWJLR0SuuWuTpwAAAh5JREFUOMtjYGBgYOcXEl6HAYSF+FgZQICJex1OwMkEVIAi3+Xh1ozM5wKaj8xfpBwcITsbWYSNgR+JtzpJYvU6jbAVSEK8DEIITpOZqnxItISWfgVCTJAB7v4ZXpKRC9uMNCqXJci6TID7hQFMrV2zJE7abTKQFesDJGb7SYTOX7sGLAVWUKCgrGZcDeaDFaxb12alqC6XDlMwTyKnRLJ1HbKCddNEc0skJkAVdEssXatRiKqgVmLlatUqqILVpuaOEnLJy4GsIhONuHlAOldVwtJWcwnMDb2i4dPKdHVKV3uqRCdYqU9psVDOmh0vUQN35FTRhevWLU+V0FeZBdTtpSQRvgAoKtuMqmBdpKxvKYjXJ+o+cx0WBRPFO6ABHuesMheLghIdePiutc7AoqBLchZchVMSFgUr9HTS8sEgL1C0E1XBRNGUeeV6OlFONjbqSjY2Nv7mKjnzMyXqYQrW2OsYS8smLkOE5OpsFSkdQ6PlUAU9EgtXq6MFdZ3EkpVKNVAFc8TKW6QbURVMFK1slOyGuSFdUkLOoQtZwSRXaRmpKLgj1y1eMjdIImguTMHCCAnvGcuXQhIMPMl1O8hnrOy31GtfnaNi3oRIcohEu7ZY20DZK0DGTCV7NVKi5UVK40vDJVatU/dfgCTEw8AsgsSdLx+TKjUdOeMAsycnMr/BzrIcmc8ByrycuDMvByM4f7PyCmLL/gK8LEBJALYsGEdXEyupAAAAAElFTkSuQmCC" type="image/x-icon">
|
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACDVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////8nbP8obf8pbf8qbv8rb/8sb/8tcP8ucf8vcf8vcv8xc/8zdP81df81dv82dv83d/84eP85eP86ef87ev89e/8+fP8/fP9Aff9Bfv9Cfv9Df/9EgP9FgP9Ggf9Hgv9Jg/9LhP9Mhf9Nhv9Oh/9Ph/9RiP9Rif9Sif9Ui/9Vi/9WjP9Xjf9Yjf9aj/9dkf9ekf9ilP9jlf9llv9nl/9omP9rmv9sm/9tnP9vnf9wnv9yn/91of92ov93o/94o/98pv9/qP+Bqf+Cqv+Eq/+FrP+Hrf+Irv+Jr/+Kr/+Msf+Nsv+Stf+Ttf+Ttv+Utv+Vt/+WuP+XuP+Zuf+Zuv+hv/+kwf+lwv+mwv+nw/+oxP+pxP+pxf+qxf+rxv+yy/+0zP+1zf+3zv+4z/+60P+70f+90v+/0/+/1P/B1f/D1v/E1//F1//F2P/G2P/H2f/I2v/J2v/K2//L3P/P3v/Q3//R4P/S4P/V4v/V4//W4//X5P/Y5P/b5v/b5//c5//d6P/e6f/f6f/g6v/h6//j7P/k7f/l7f/m7v/q8f/r8f/u8//w9f/x9f/x9v/z9//0+P/2+f/3+f/3+v/4+v/5+//6/P/8/f/9/v/+/v////9uCbVDAAAAFXRSTlMABAU4Ozw9PpSWl5ilp6ip4+Tl/P6nIcp/AAAAAWJLR0SuuWuTpwAAAh5JREFUOMtjYGBgYOcXEl6HAYSF+FgZQICJex1OwMkEVIAi3+Xh1ozM5wKaj8xfpBwcITsbWYSNgR+JtzpJYvU6jbAVSEK8DEIITpOZqnxItISWfgVCTJAB7v4ZXpKRC9uMNCqXJci6TID7hQFMrV2zJE7abTKQFesDJGb7SYTOX7sGLAVWUKCgrGZcDeaDFaxb12alqC6XDlMwTyKnRLJ1HbKCddNEc0skJkAVdEssXatRiKqgVmLlatUqqILVpuaOEnLJy4GsIhONuHlAOldVwtJWcwnMDb2i4dPKdHVKV3uqRCdYqU9psVDOmh0vUQN35FTRhevWLU+V0FeZBdTtpSQRvgAoKtuMqmBdpKxvKYjXJ+o+cx0WBRPFO6ABHuesMheLghIdePiutc7AoqBLchZchVMSFgUr9HTS8sEgL1C0E1XBRNGUeeV6OlFONjbqSjY2Nv7mKjnzMyXqYQrW2OsYS8smLkOE5OpsFSkdQ6PlUAU9EgtXq6MFdZ3EkpVKNVAFc8TKW6QbURVMFK1slOyGuSFdUkLOoQtZwSRXaRmpKLgj1y1eMjdIImguTMHCCAnvGcuXQhIMPMl1O8hnrOy31GtfnaNi3oRIcohEu7ZY20DZK0DGTCV7NVKi5UVK40vDJVatU/dfgCTEw8AsgsSdLx+TKjUdOeMAsycnMr/BzrIcmc8ByrycuDMvByM4f7PyCmLL/gK8LEBJALYsGEdXEyupAAAAAElFTkSuQmCC" type="image/x-icon">
|
||||||
<link rel="dns-prefetch" href="https://js.stripe.com">
|
<link rel="dns-prefetch" href="https://js.stripe.com">
|
||||||
</head>
|
</head>
|
||||||
|
Loading…
Reference in New Issue
Block a user