storj/satellite/console/consoleweb/server.go
stefanbenten 9ace375ee0 satellite/{console,satellitedb}: change project limiting based on new users field
This change switches the backend logic to use the new DB column on the users table to restrict project creation.
Furthermore it back fills the existing limits from registration tokens to the new column to ensure no users are reset to the new default.

UI is updated to reflect ability to create several projects

Change-Id: Ie29157430ae6b065411ca4c4557c9f1be69cdc4f
2020-07-16 10:57:47 +00:00

835 lines
26 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleweb
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
"html/template"
"mime"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/gqlerrors"
monkit "github.com/spacemonkeygo/monkit/v3"
"github.com/zeebo/errs"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"storj.io/common/errs2"
"storj.io/common/uuid"
"storj.io/storj/pkg/auth"
"storj.io/storj/private/web"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/console/consoleweb/consoleapi"
"storj.io/storj/satellite/console/consoleweb/consoleql"
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
"storj.io/storj/satellite/mailservice"
"storj.io/storj/satellite/referrals"
)
const (
contentType = "Content-Type"
applicationJSON = "application/json"
applicationGraphql = "application/graphql"
)
var (
// Error is satellite console error type
Error = errs.Class("satellite console error")
mon = monkit.Package()
)
// Config contains configuration for console web server
type Config struct {
Address string `help:"server address of the graphql api gateway and frontend app" devDefault:"127.0.0.1:8081" releaseDefault:":10100"`
StaticDir string `help:"path to static resources" default:""`
ExternalAddress string `help:"external endpoint of the satellite if hosted" default:""`
// TODO: remove after Vanguard release
AuthToken string `help:"auth token needed for access to registration token creation endpoint" default:""`
AuthTokenSecret string `help:"secret used to sign auth tokens" releaseDefault:"" devDefault:"my-suppa-secret-key"`
ContactInfoURL string `help:"url link to contacts page" default:"https://forum.storj.io"`
FrameAncestors string `help:"allow domains to embed the satellite in a frame, space separated" default:"tardigrade.io"`
LetUsKnowURL string `help:"url link to let us know page" default:"https://storjlabs.atlassian.net/servicedesk/customer/portals"`
SEO string `help:"used to communicate with web crawlers and other web robots" default:"User-agent: *\nDisallow: \nDisallow: /cgi-bin/"`
SatelliteName string `help:"used to display at web satellite console" default:"Storj"`
SatelliteOperator string `help:"name of organization which set up satellite" default:"Storj Labs" `
TermsAndConditionsURL string `help:"url link to terms and conditions page" default:"https://storj.io/storage-sla/"`
SegmentIOPublicKey string `help:"used to initialize segment.io at web satellite console" default:""`
AccountActivationRedirectURL string `help:"url link for account activation redirect" default:""`
VerificationPageURL string `help:"url link to sign up verification page" default:"https://tardigrade.io/verify"`
PartneredSatelliteNames string `help:"names of partnered satellites" default:"US-Central-1,Europe-West-1,Asia-East-1"`
GoogleTagManagerID string `help:"id for google tag manager" default:""`
RateLimit web.IPRateLimiterConfig
console.Config
}
// Server represents console web server
//
// architecture: Endpoint
type Server struct {
log *zap.Logger
config Config
service *console.Service
mailService *mailservice.Service
referralsService *referrals.Service
listener net.Listener
server http.Server
cookieAuth *consolewebauth.CookieAuth
rateLimiter *web.IPRateLimiter
stripePublicKey string
schema graphql.Schema
templates struct {
index *template.Template
notFound *template.Template
internalServerError *template.Template
usageReport *template.Template
resetPassword *template.Template
success *template.Template
activated *template.Template
}
}
// NewServer creates new instance of console server.
func NewServer(logger *zap.Logger, config Config, service *console.Service, mailService *mailservice.Service, referralsService *referrals.Service, listener net.Listener, stripePublicKey string) *Server {
server := Server{
log: logger,
config: config,
listener: listener,
service: service,
mailService: mailService,
referralsService: referralsService,
stripePublicKey: stripePublicKey,
rateLimiter: web.NewIPRateLimiter(config.RateLimit),
}
logger.Debug("Starting Satellite UI.", zap.Stringer("Address", server.listener.Addr()))
server.cookieAuth = consolewebauth.NewCookieAuth(consolewebauth.CookieSettings{
Name: "_tokenKey",
Path: "/",
})
if server.config.ExternalAddress != "" {
if !strings.HasSuffix(server.config.ExternalAddress, "/") {
server.config.ExternalAddress += "/"
}
} else {
server.config.ExternalAddress = "http://" + server.listener.Addr().String() + "/"
}
if server.config.AccountActivationRedirectURL == "" {
server.config.AccountActivationRedirectURL = server.config.ExternalAddress + "login?activated=true"
}
router := mux.NewRouter()
fs := http.FileServer(http.Dir(server.config.StaticDir))
router.HandleFunc("/registrationToken/", server.createRegistrationTokenHandler)
router.HandleFunc("/populate-promotional-coupons", server.populatePromotionalCoupons).Methods(http.MethodPost)
router.HandleFunc("/robots.txt", server.seoHandler)
router.Handle("/api/v0/graphql", server.withAuth(http.HandlerFunc(server.grapqlHandler)))
router.Handle(
"/api/v0/projects/{id}/usage-limits",
server.withAuth(http.HandlerFunc(server.projectUsageLimitsHandler)),
).Methods(http.MethodGet)
referralsController := consoleapi.NewReferrals(logger, referralsService, service, mailService, server.config.ExternalAddress)
referralsRouter := router.PathPrefix("/api/v0/referrals").Subrouter()
referralsRouter.Handle("/tokens", server.withAuth(http.HandlerFunc(referralsController.GetTokens))).Methods(http.MethodGet)
referralsRouter.HandleFunc("/register", referralsController.Register).Methods(http.MethodPost)
authController := consoleapi.NewAuth(logger, service, mailService, server.cookieAuth, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL)
authRouter := router.PathPrefix("/api/v0/auth").Subrouter()
authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.GetAccount))).Methods(http.MethodGet)
authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.UpdateAccount))).Methods(http.MethodPatch)
authRouter.Handle("/account/change-password", server.withAuth(http.HandlerFunc(authController.ChangePassword))).Methods(http.MethodPost)
authRouter.Handle("/account/delete", server.withAuth(http.HandlerFunc(authController.DeleteAccount))).Methods(http.MethodPost)
authRouter.HandleFunc("/logout", authController.Logout).Methods(http.MethodPost)
authRouter.Handle("/token", server.rateLimiter.Limit(http.HandlerFunc(authController.Token))).Methods(http.MethodPost)
authRouter.Handle("/register", server.rateLimiter.Limit(http.HandlerFunc(authController.Register))).Methods(http.MethodPost)
authRouter.Handle("/forgot-password/{email}", server.rateLimiter.Limit(http.HandlerFunc(authController.ForgotPassword))).Methods(http.MethodPost)
authRouter.Handle("/resend-email/{id}", server.rateLimiter.Limit(http.HandlerFunc(authController.ResendEmail))).Methods(http.MethodPost)
paymentController := consoleapi.NewPayments(logger, service)
paymentsRouter := router.PathPrefix("/api/v0/payments").Subrouter()
paymentsRouter.Use(server.withAuth)
paymentsRouter.HandleFunc("/cards", paymentController.AddCreditCard).Methods(http.MethodPost)
paymentsRouter.HandleFunc("/cards", paymentController.MakeCreditCardDefault).Methods(http.MethodPatch)
paymentsRouter.HandleFunc("/cards", paymentController.ListCreditCards).Methods(http.MethodGet)
paymentsRouter.HandleFunc("/cards/{cardId}", paymentController.RemoveCreditCard).Methods(http.MethodDelete)
paymentsRouter.HandleFunc("/account/charges", paymentController.ProjectsCharges).Methods(http.MethodGet)
paymentsRouter.HandleFunc("/account/balance", paymentController.AccountBalance).Methods(http.MethodGet)
paymentsRouter.HandleFunc("/account", paymentController.SetupAccount).Methods(http.MethodPost)
paymentsRouter.HandleFunc("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet)
paymentsRouter.HandleFunc("/tokens/deposit", paymentController.TokenDeposit).Methods(http.MethodPost)
if server.config.StaticDir != "" {
router.HandleFunc("/activation/", server.accountActivationHandler)
router.HandleFunc("/password-recovery/", server.passwordRecoveryHandler)
router.HandleFunc("/cancel-password-recovery/", server.cancelPasswordRecoveryHandler)
router.HandleFunc("/usage-report", server.bucketUsageReportHandler)
router.PathPrefix("/static/").Handler(server.gzipMiddleware(http.StripPrefix("/static", fs)))
router.PathPrefix("/").Handler(http.HandlerFunc(server.appHandler))
}
server.server = http.Server{
Handler: router,
MaxHeaderBytes: ContentLengthLimit.Int(),
}
return &server
}
// Run starts the server that host webapp and api endpoint.
func (server *Server) Run(ctx context.Context) (err error) {
defer mon.Task()(&ctx)(&err)
server.schema, err = consoleql.CreateSchema(server.log, server.service, server.mailService)
if err != nil {
return Error.Wrap(err)
}
err = server.initializeTemplates()
if err != nil {
// TODO: should it return error if some template can not be initialized or just log about it?
return Error.Wrap(err)
}
ctx, cancel := context.WithCancel(ctx)
var group errgroup.Group
group.Go(func() error {
<-ctx.Done()
return server.server.Shutdown(context.Background())
})
group.Go(func() error {
server.rateLimiter.Run(ctx)
return nil
})
group.Go(func() error {
defer cancel()
err := server.server.Serve(server.listener)
if errs2.IsCanceled(err) || errors.Is(err, http.ErrServerClosed) {
err = nil
}
return err
})
return group.Wait()
}
// Close closes server and underlying listener
func (server *Server) Close() error {
return server.server.Close()
}
// appHandler is web app http handler function
func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
header := w.Header()
cspValues := []string{
"default-src 'self'",
"connect-src 'self' api.segment.io *.google-analytics.com",
"frame-ancestors " + server.config.FrameAncestors,
"frame-src 'self' *.stripe.com *.googletagmanager.com",
"img-src 'self' data: *.customer.io *.googletagmanager.com *.google-analytics.com",
"script-src 'sha256-wAqYV6m2PHGd1WDyFBnZmSoyfCK0jxFAns0vGbdiWUA=' 'self' *.stripe.com cdn.segment.com *.customer.io *.google-analytics.com *.googletagmanager.com",
}
header.Set(contentType, "text/html; charset=UTF-8")
header.Set("Content-Security-Policy", strings.Join(cspValues, "; "))
header.Set("X-Content-Type-Options", "nosniff")
header.Set("Referrer-Policy", "same-origin") // Only expose the referring url when navigating around the satellite itself.
var data struct {
SatelliteName string
SegmentIOPublicKey string
StripePublicKey string
VerificationPageURL string
PartneredSatelliteNames string
GoogleTagManagerID string
DefaultProjectLimit int
}
data.SatelliteName = server.config.SatelliteName
data.SegmentIOPublicKey = server.config.SegmentIOPublicKey
data.StripePublicKey = server.stripePublicKey
data.VerificationPageURL = server.config.VerificationPageURL
data.PartneredSatelliteNames = server.config.PartneredSatelliteNames
data.GoogleTagManagerID = server.config.GoogleTagManagerID
data.DefaultProjectLimit = server.config.DefaultProjectLimit
if server.templates.index == nil || server.templates.index.Execute(w, data) != nil {
server.log.Error("index template could not be executed")
return
}
}
// authMiddlewareHandler performs initial authorization before every request.
func (server *Server) withAuth(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
var ctx context.Context
defer mon.Task()(&ctx)(&err)
ctxWithAuth := func(ctx context.Context) context.Context {
token, err := server.cookieAuth.GetToken(r)
if err != nil {
return console.WithAuthFailure(ctx, err)
}
ctx = auth.WithAPIKey(ctx, []byte(token))
auth, err := server.service.Authorize(ctx)
if err != nil {
return console.WithAuthFailure(ctx, err)
}
return console.WithAuth(ctx, auth)
}
ctx = ctxWithAuth(r.Context())
handler.ServeHTTP(w, r.Clone(ctx))
})
}
// bucketUsageReportHandler generate bucket usage report page for project.
func (server *Server) bucketUsageReportHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
token, err := server.cookieAuth.GetToken(r)
if err != nil {
server.serveError(w, http.StatusUnauthorized)
return
}
auth, err := server.service.Authorize(auth.WithAPIKey(ctx, []byte(token)))
if err != nil {
server.serveError(w, http.StatusUnauthorized)
return
}
ctx = console.WithAuth(ctx, auth)
// parse query params
projectID, err := uuid.FromString(r.URL.Query().Get("projectID"))
if err != nil {
server.serveError(w, http.StatusBadRequest)
return
}
sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64)
if err != nil {
server.serveError(w, http.StatusBadRequest)
return
}
beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("before"), 10, 64)
if err != nil {
server.serveError(w, http.StatusBadRequest)
return
}
since := time.Unix(sinceStamp, 0).UTC()
before := time.Unix(beforeStamp, 0).UTC()
server.log.Debug("querying bucket usage report",
zap.Stringer("projectID", projectID),
zap.Stringer("since", since),
zap.Stringer("before", before))
bucketRollups, err := server.service.GetBucketUsageRollups(ctx, projectID, since, before)
if err != nil {
server.log.Error("bucket usage report error", zap.Error(err))
server.serveError(w, http.StatusInternalServerError)
return
}
if err = server.templates.usageReport.Execute(w, bucketRollups); err != nil {
server.log.Error("bucket usage report error", zap.Error(err))
}
}
// createRegistrationTokenHandler is web app http handler function.
func (server *Server) createRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer mon.Task()(&ctx)(nil)
w.Header().Set(contentType, applicationJSON)
var response struct {
Secret string `json:"secret"`
Error string `json:"error,omitempty"`
}
defer func() {
err := json.NewEncoder(w).Encode(&response)
if err != nil {
server.log.Error(err.Error())
}
}()
equality := subtle.ConstantTimeCompare(
[]byte(r.Header.Get("Authorization")),
[]byte(server.config.AuthToken),
)
if equality != 1 {
w.WriteHeader(401)
response.Error = "unauthorized"
return
}
projectsLimitInput := r.URL.Query().Get("projectsLimit")
projectsLimit, err := strconv.Atoi(projectsLimitInput)
if err != nil {
response.Error = err.Error()
return
}
token, err := server.service.CreateRegToken(ctx, projectsLimit)
if err != nil {
response.Error = err.Error()
return
}
response.Secret = token.Secret.String()
}
// populatePromotionalCoupons is web app http handler function for populating promotional coupons.
func (server *Server) populatePromotionalCoupons(w http.ResponseWriter, r *http.Request) {
var err error
var ctx context.Context
defer mon.Task()(&ctx)(&err)
handleError := func(status int, err error) {
w.WriteHeader(status)
w.Header().Set(contentType, applicationJSON)
var response struct {
Error string `json:"error"`
}
response.Error = err.Error()
if err := json.NewEncoder(w).Encode(response); err != nil {
server.log.Error("failed to write json error response", zap.Error(Error.Wrap(err)))
}
}
ctx = r.Context()
equality := subtle.ConstantTimeCompare(
[]byte(r.Header.Get("Authorization")),
[]byte(server.config.AuthToken),
)
if equality != 1 {
handleError(http.StatusUnauthorized, errs.New("unauthorized"))
return
}
if err = server.service.Payments().PopulatePromotionalCoupons(ctx); err != nil {
handleError(http.StatusInternalServerError, err)
return
}
}
// accountActivationHandler is web app http handler function
func (server *Server) accountActivationHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer mon.Task()(&ctx)(nil)
activationToken := r.URL.Query().Get("token")
err := server.service.ActivateAccount(ctx, activationToken)
if err != nil {
server.log.Error("activation: failed to activate account",
zap.String("token", activationToken),
zap.Error(err))
if console.ErrEmailUsed.Has(err) {
server.serveError(w, http.StatusConflict)
return
}
if console.Error.Has(err) {
server.serveError(w, http.StatusInternalServerError)
return
}
server.serveError(w, http.StatusNotFound)
return
}
http.Redirect(w, r, server.config.AccountActivationRedirectURL, http.StatusTemporaryRedirect)
}
func (server *Server) passwordRecoveryHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer mon.Task()(&ctx)(nil)
recoveryToken := r.URL.Query().Get("token")
if len(recoveryToken) == 0 {
server.serveError(w, http.StatusNotFound)
return
}
var data struct {
SatelliteName string
}
data.SatelliteName = server.config.SatelliteName
switch r.Method {
case http.MethodPost:
err := r.ParseForm()
if err != nil {
server.serveError(w, http.StatusNotFound)
return
}
password := r.FormValue("password")
passwordRepeat := r.FormValue("passwordRepeat")
if strings.Compare(password, passwordRepeat) != 0 {
server.serveError(w, http.StatusNotFound)
return
}
err = server.service.ResetPassword(ctx, recoveryToken, password)
if err != nil {
server.serveError(w, http.StatusNotFound)
return
}
if err := server.templates.success.Execute(w, data); err != nil {
server.log.Error("success reset password template could not be executed", zap.Error(Error.Wrap(err)))
return
}
case http.MethodGet:
if err := server.templates.resetPassword.Execute(w, data); err != nil {
server.log.Error("reset password template could not be executed", zap.Error(Error.Wrap(err)))
return
}
default:
server.serveError(w, http.StatusNotFound)
return
}
}
func (server *Server) cancelPasswordRecoveryHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer mon.Task()(&ctx)(nil)
recoveryToken := r.URL.Query().Get("token")
// No need to check error as we anyway redirect user to support page
_ = server.service.RevokeResetPasswordToken(ctx, recoveryToken)
// TODO: Should place this link to config
http.Redirect(w, r, "https://storjlabs.atlassian.net/servicedesk/customer/portals", http.StatusSeeOther)
}
// projectUsageLimitsHandler api handler for project usage limits.
func (server *Server) projectUsageLimitsHandler(w http.ResponseWriter, r *http.Request) {
err := error(nil)
ctx := r.Context()
defer mon.Task()(&ctx)(&err)
var ok bool
var idParam string
handleError := func(code int, err error) {
w.WriteHeader(code)
var jsonError struct {
Error string `json:"error"`
}
jsonError.Error = err.Error()
if err := json.NewEncoder(w).Encode(jsonError); err != nil {
server.log.Error("error encoding project usage limits error", zap.Error(err))
}
}
handleServiceError := func(err error) {
switch {
case console.ErrUnauthorized.Has(err):
handleError(http.StatusUnauthorized, err)
default:
handleError(http.StatusInternalServerError, err)
}
}
w.Header().Set("Content-Type", "application/json")
if idParam, ok = mux.Vars(r)["id"]; !ok {
handleError(http.StatusBadRequest, errs.New("missing project id route param"))
return
}
projectID, err := uuid.FromString(idParam)
if err != nil {
handleError(http.StatusBadRequest, errs.New("invalid project id: %v", err))
return
}
limits, err := server.service.GetProjectUsageLimits(ctx, projectID)
if err != nil {
handleServiceError(err)
return
}
if err := json.NewEncoder(w).Encode(limits); err != nil {
server.log.Error("error encoding project usage limits", zap.Error(err))
return
}
}
// grapqlHandler is graphql endpoint http handler function
func (server *Server) grapqlHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer mon.Task()(&ctx)(nil)
handleError := func(code int, err error) {
w.WriteHeader(code)
var jsonError struct {
Error string `json:"error"`
}
jsonError.Error = err.Error()
if err := json.NewEncoder(w).Encode(jsonError); err != nil {
server.log.Error("error graphql error", zap.Error(err))
}
}
w.Header().Set(contentType, applicationJSON)
query, err := getQuery(w, r)
if err != nil {
handleError(http.StatusBadRequest, err)
return
}
rootObject := make(map[string]interface{})
rootObject["origin"] = server.config.ExternalAddress
rootObject[consoleql.ActivationPath] = "activation/?token="
rootObject[consoleql.PasswordRecoveryPath] = "password-recovery/?token="
rootObject[consoleql.CancelPasswordRecoveryPath] = "cancel-password-recovery/?token="
rootObject[consoleql.SignInPath] = "login"
rootObject[consoleql.LetUsKnowURL] = server.config.LetUsKnowURL
rootObject[consoleql.ContactInfoURL] = server.config.ContactInfoURL
rootObject[consoleql.TermsAndConditionsURL] = server.config.TermsAndConditionsURL
result := graphql.Do(graphql.Params{
Schema: server.schema,
Context: ctx,
RequestString: query.Query,
VariableValues: query.Variables,
OperationName: query.OperationName,
RootObject: rootObject,
})
getGqlError := func(err gqlerrors.FormattedError) error {
if gerr, ok := err.OriginalError().(*gqlerrors.Error); ok {
return gerr.OriginalError
}
return nil
}
parseConsoleError := func(err error) (int, error) {
switch {
case console.ErrUnauthorized.Has(err):
return http.StatusUnauthorized, err
case console.Error.Has(err):
return http.StatusInternalServerError, err
}
return 0, nil
}
handleErrors := func(code int, errors gqlerrors.FormattedErrors) {
w.WriteHeader(code)
var jsonError struct {
Errors []string `json:"errors"`
}
for _, err := range errors {
jsonError.Errors = append(jsonError.Errors, err.Message)
}
if err := json.NewEncoder(w).Encode(jsonError); err != nil {
server.log.Error("error graphql error", zap.Error(err))
}
}
handleGraphqlErrors := func() {
for _, err := range result.Errors {
gqlErr := getGqlError(err)
if gqlErr == nil {
continue
}
code, err := parseConsoleError(gqlErr)
if err != nil {
handleError(code, err)
return
}
}
handleErrors(http.StatusOK, result.Errors)
}
if result.HasErrors() {
handleGraphqlErrors()
return
}
err = json.NewEncoder(w).Encode(result)
if err != nil {
server.log.Error("error encoding grapql result", zap.Error(err))
return
}
server.log.Debug(fmt.Sprintf("%s", result))
}
// serveError serves error static pages.
func (server *Server) serveError(w http.ResponseWriter, status int) {
w.WriteHeader(status)
switch status {
case http.StatusInternalServerError:
err := server.templates.internalServerError.Execute(w, nil)
if err != nil {
server.log.Error("cannot parse internalServerError template", zap.Error(Error.Wrap(err)))
}
case http.StatusNotFound:
err := server.templates.notFound.Execute(w, nil)
if err != nil {
server.log.Error("cannot parse pageNotFound template", zap.Error(Error.Wrap(err)))
}
case http.StatusConflict:
err := server.templates.activated.Execute(w, nil)
if err != nil {
server.log.Error("cannot parse already activated template", zap.Error(Error.Wrap(err)))
}
}
}
// seoHandler used to communicate with web crawlers and other web robots
func (server *Server) seoHandler(w http.ResponseWriter, req *http.Request) {
header := w.Header()
header.Set(contentType, mime.TypeByExtension(".txt"))
header.Set("X-Content-Type-Options", "nosniff")
_, err := w.Write([]byte(server.config.SEO))
if err != nil {
server.log.Error(err.Error())
}
}
// gzipMiddleware is used to gzip static content to minify resources if browser support such decoding.
func (server *Server) gzipMiddleware(fn http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Set("X-Content-Type-Options", "nosniff")
isGzipSupported := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
if !isGzipSupported {
fn.ServeHTTP(w, r)
return
}
info, err := os.Stat(server.config.StaticDir + strings.TrimPrefix(r.URL.Path, "/static") + ".gz")
if err != nil {
fn.ServeHTTP(w, r)
return
}
extension := filepath.Ext(info.Name()[:len(info.Name())-3])
w.Header().Set(contentType, mime.TypeByExtension(extension))
w.Header().Set("Content-Encoding", "gzip")
newRequest := new(http.Request)
*newRequest = *r
newRequest.URL = new(url.URL)
*newRequest.URL = *r.URL
newRequest.URL.Path += ".gz"
fn.ServeHTTP(w, newRequest)
})
}
// initializeTemplates is used to initialize all templates
func (server *Server) initializeTemplates() (err error) {
server.templates.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))
}
server.templates.activated, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "activation", "activated.html"))
if err != nil {
return Error.Wrap(err)
}
server.templates.success, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "resetPassword", "success.html"))
if err != nil {
return Error.Wrap(err)
}
server.templates.resetPassword, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "resetPassword", "resetPassword.html"))
if err != nil {
return Error.Wrap(err)
}
server.templates.usageReport, err = template.ParseFiles(path.Join(server.config.StaticDir, "static", "reports", "usageReport.html"))
if err != nil {
return Error.Wrap(err)
}
server.templates.notFound, err = template.ParseFiles(path.Join(server.config.StaticDir, "static", "errors", "404.html"))
if err != nil {
return Error.Wrap(err)
}
server.templates.internalServerError, err = template.ParseFiles(path.Join(server.config.StaticDir, "static", "errors", "500.html"))
if err != nil {
return Error.Wrap(err)
}
return nil
}