storj/satellite/console/consoleweb/server.go
Jeff Wendling 7999d24f81 all: use monkit v3
this commit updates our monkit dependency to the v3 version where
it outputs in an influx style. this makes discovery much easier
as many tools are built to look at it this way.

graphite and rothko will suffer some due to no longer being a tree
based on dots. hopefully time will exist to update rothko to
index based on the new metric format.

it adds an influx output for the statreceiver so that we can
write to influxdb v1 or v2 directly.

Change-Id: Iae9f9494a6d29cfbd1f932a5e71a891b490415ff
2020-02-05 23:53:17 +00:00

794 lines
24 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleweb
import (
"context"
"crypto/subtle"
"encoding/json"
"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"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/spacemonkeygo/monkit/v3"
"github.com/zeebo/errs"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"storj.io/storj/pkg/auth"
"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"`
PasswordCost int `internal:"true" help:"password hashing cost (0=automatic)" default:"0"`
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:""`
}
// 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
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,
}
logger.Sugar().Debugf("Starting Satellite UI on %s...", server.listener.Addr().String())
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() + "/"
}
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.Handle("/logout", server.withAuth(http.HandlerFunc(authController.Logout))).Methods(http.MethodPost)
authRouter.HandleFunc("/token", authController.Token).Methods(http.MethodPost)
authRouter.HandleFunc("/register", authController.Register).Methods(http.MethodPost)
authRouter.HandleFunc("/forgot-password/{email}", authController.ForgotPassword).Methods(http.MethodPost)
authRouter.HandleFunc("/resend-email/{id}", 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 {
defer cancel()
return server.server.Serve(server.listener)
})
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",
"frame-ancestors " + server.config.FrameAncestors,
"frame-src 'self' *.stripe.com",
"img-src 'self' data: *.customer.io",
"script-src 'self' *.stripe.com cdn.segment.com *.customer.io",
}
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
}
data.SatelliteName = server.config.SatelliteName
data.SegmentIOPublicKey = server.config.SegmentIOPublicKey
data.StripePublicKey = server.stripePublicKey
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.Parse(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))
// TODO: when new error pages will be created - change http.StatusNotFound on appropriate one
server.serveError(w, http.StatusNotFound)
return
}
if err = server.templates.activated.Execute(w, nil); err != nil {
server.log.Error("account activated template could not be executed", zap.Error(Error.Wrap(err)))
return
}
}
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.Parse(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.ErrValidation.Has(err):
return http.StatusBadRequest, 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.StatusBadRequest, 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.Sugar().Debug(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)))
}
default:
err := server.templates.notFound.Execute(w, nil)
if err != nil {
server.log.Error("cannot parse pageNotFound 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
}