1// Copyright (C) 2019 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package consoleweb
5
6import (
7	"context"
8	"crypto/subtle"
9	"encoding/json"
10	"errors"
11	"fmt"
12	"html/template"
13	"mime"
14	"net"
15	"net/http"
16	"net/url"
17	"os"
18	"path/filepath"
19	"strconv"
20	"strings"
21	"time"
22
23	"github.com/gorilla/mux"
24	"github.com/graphql-go/graphql"
25	"github.com/graphql-go/graphql/gqlerrors"
26	"github.com/spacemonkeygo/monkit/v3"
27	"github.com/zeebo/errs"
28	"go.uber.org/zap"
29	"golang.org/x/sync/errgroup"
30
31	"storj.io/common/errs2"
32	"storj.io/common/memory"
33	"storj.io/common/storj"
34	"storj.io/common/uuid"
35	"storj.io/storj/private/web"
36	"storj.io/storj/satellite/analytics"
37	"storj.io/storj/satellite/console"
38	"storj.io/storj/satellite/console/consoleauth"
39	"storj.io/storj/satellite/console/consoleweb/consoleapi"
40	"storj.io/storj/satellite/console/consoleweb/consoleql"
41	"storj.io/storj/satellite/console/consoleweb/consolewebauth"
42	"storj.io/storj/satellite/mailservice"
43	"storj.io/storj/satellite/payments/paymentsconfig"
44	"storj.io/storj/satellite/rewards"
45)
46
47const (
48	contentType = "Content-Type"
49
50	applicationJSON    = "application/json"
51	applicationGraphql = "application/graphql"
52)
53
54var (
55	// Error is satellite console error type.
56	Error = errs.Class("consoleweb")
57
58	mon = monkit.Package()
59)
60
61// Config contains configuration for console web server.
62type Config struct {
63	Address         string `help:"server address of the graphql api gateway and frontend app" devDefault:"127.0.0.1:0" releaseDefault:":10100"`
64	StaticDir       string `help:"path to static resources" default:""`
65	Watch           bool   `help:"whether to load templates on each request" default:"false" devDefault:"true"`
66	ExternalAddress string `help:"external endpoint of the satellite if hosted" default:""`
67
68	// TODO: remove after Vanguard release
69	AuthToken       string `help:"auth token needed for access to registration token creation endpoint" default:"" testDefault:"very-secret-token"`
70	AuthTokenSecret string `help:"secret used to sign auth tokens" releaseDefault:"" devDefault:"my-suppa-secret-key"`
71
72	ContactInfoURL                  string  `help:"url link to contacts page" default:"https://forum.storj.io"`
73	FrameAncestors                  string  `help:"allow domains to embed the satellite in a frame, space separated" default:"tardigrade.io storj.io"`
74	LetUsKnowURL                    string  `help:"url link to let us know page" default:"https://storjlabs.atlassian.net/servicedesk/customer/portals"`
75	SEO                             string  `help:"used to communicate with web crawlers and other web robots" default:"User-agent: *\nDisallow: \nDisallow: /cgi-bin/"`
76	SatelliteName                   string  `help:"used to display at web satellite console" default:"Storj"`
77	SatelliteOperator               string  `help:"name of organization which set up satellite" default:"Storj Labs" `
78	TermsAndConditionsURL           string  `help:"url link to terms and conditions page" default:"https://storj.io/storage-sla/"`
79	AccountActivationRedirectURL    string  `help:"url link for account activation redirect" default:""`
80	PartneredSatellites             SatList `help:"names and addresses of partnered satellites in JSON list format" default:"[[\"US1\",\"https://us1.storj.io\"],[\"EU1\",\"https://eu1.storj.io\"],[\"AP1\",\"https://ap1.storj.io\"]]"`
81	GeneralRequestURL               string  `help:"url link to general request page" default:"https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000379291"`
82	ProjectLimitsIncreaseRequestURL string  `help:"url link to project limit increase request page" default:"https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000683212"`
83	GatewayCredentialsRequestURL    string  `help:"url link for gateway credentials requests" default:"https://auth.us1.storjshare.io" devDefault:""`
84	IsBetaSatellite                 bool    `help:"indicates if satellite is in beta" default:"false"`
85	BetaSatelliteFeedbackURL        string  `help:"url link for for beta satellite feedback" default:""`
86	BetaSatelliteSupportURL         string  `help:"url link for for beta satellite support" default:""`
87	DocumentationURL                string  `help:"url link to documentation" default:"https://docs.storj.io/"`
88	CouponCodeBillingUIEnabled      bool    `help:"indicates if user is allowed to add coupon codes to account from billing" default:"false"`
89	CouponCodeSignupUIEnabled       bool    `help:"indicates if user is allowed to add coupon codes to account from signup" default:"false"`
90	FileBrowserFlowDisabled         bool    `help:"indicates if file browser flow is disabled" default:"false"`
91	CSPEnabled                      bool    `help:"indicates if Content Security Policy is enabled" devDefault:"false" releaseDefault:"true"`
92	LinksharingURL                  string  `help:"url link for linksharing requests" default:"https://link.us1.storjshare.io"`
93	PathwayOverviewEnabled          bool    `help:"indicates if the overview onboarding step should render with pathways" default:"true"`
94	NewProjectDashboard             bool    `help:"indicates if new project dashboard should be used" default:"false"`
95	NewOnboarding                   bool    `help:"indicates if new onboarding flow should be rendered" default:"true"`
96	NewNavigation                   bool    `help:"indicates if new navigation structure should be rendered" default:"true"`
97	NewBrowser                      bool    `help:"indicates if new browser should be used" default:"true"`
98	NewObjectsFlow                  bool    `help:"indicates if new objects flow should be used" default:"true"`
99
100	// RateLimit defines the configuration for the IP and userID rate limiters.
101	RateLimit web.RateLimiterConfig
102
103	console.Config
104}
105
106// SatList is a configuration value that contains a list of satellite names and addresses.
107// Format should be [[name,address],[name,address],...] in valid JSON format.
108//
109// Can be used as a flag.
110type SatList string
111
112// Type implements pflag.Value.
113func (SatList) Type() string { return "consoleweb.SatList" }
114
115// String is required for pflag.Value.
116func (sl *SatList) String() string {
117	return string(*sl)
118}
119
120// Set does validation on the configured JSON, but does not actually transform it - it will be passed to the client as-is.
121func (sl *SatList) Set(s string) error {
122	satellites := make([][]string, 3)
123
124	err := json.Unmarshal([]byte(s), &satellites)
125	if err != nil {
126		return err
127	}
128
129	for _, sat := range satellites {
130		if len(sat) != 2 {
131			return errs.New("Could not parse satellite list config. Each satellite in the config must have two values: [name, address]")
132		}
133	}
134
135	*sl = SatList(s)
136	return nil
137}
138
139// Server represents console web server.
140//
141// architecture: Endpoint
142type Server struct {
143	log *zap.Logger
144
145	config      Config
146	service     *console.Service
147	mailService *mailservice.Service
148	partners    *rewards.PartnersService
149	analytics   *analytics.Service
150
151	listener          net.Listener
152	server            http.Server
153	cookieAuth        *consolewebauth.CookieAuth
154	ipRateLimiter     *web.RateLimiter
155	userIDRateLimiter *web.RateLimiter
156	nodeURL           storj.NodeURL
157
158	stripePublicKey string
159
160	pricing paymentsconfig.PricingValues
161
162	schema graphql.Schema
163
164	templatesCache *templates
165}
166
167type templates struct {
168	index               *template.Template
169	notFound            *template.Template
170	internalServerError *template.Template
171	usageReport         *template.Template
172}
173
174// NewServer creates new instance of console server.
175func NewServer(logger *zap.Logger, config Config, service *console.Service, mailService *mailservice.Service, partners *rewards.PartnersService, analytics *analytics.Service, listener net.Listener, stripePublicKey string, pricing paymentsconfig.PricingValues, nodeURL storj.NodeURL) *Server {
176	server := Server{
177		log:               logger,
178		config:            config,
179		listener:          listener,
180		service:           service,
181		mailService:       mailService,
182		partners:          partners,
183		analytics:         analytics,
184		stripePublicKey:   stripePublicKey,
185		ipRateLimiter:     web.NewIPRateLimiter(config.RateLimit),
186		userIDRateLimiter: NewUserIDRateLimiter(config.RateLimit),
187		nodeURL:           nodeURL,
188		pricing:           pricing,
189	}
190
191	logger.Debug("Starting Satellite UI.", zap.Stringer("Address", server.listener.Addr()))
192
193	server.cookieAuth = consolewebauth.NewCookieAuth(consolewebauth.CookieSettings{
194		Name: "_tokenKey",
195		Path: "/",
196	})
197
198	if server.config.ExternalAddress != "" {
199		if !strings.HasSuffix(server.config.ExternalAddress, "/") {
200			server.config.ExternalAddress += "/"
201		}
202	} else {
203		server.config.ExternalAddress = "http://" + server.listener.Addr().String() + "/"
204	}
205
206	if server.config.AccountActivationRedirectURL == "" {
207		server.config.AccountActivationRedirectURL = server.config.ExternalAddress + "login?activated=true"
208	}
209
210	router := mux.NewRouter()
211	fs := http.FileServer(http.Dir(server.config.StaticDir))
212
213	router.HandleFunc("/registrationToken/", server.createRegistrationTokenHandler)
214	router.HandleFunc("/robots.txt", server.seoHandler)
215
216	router.Handle("/api/v0/graphql", server.withAuth(http.HandlerFunc(server.graphqlHandler)))
217
218	usageLimitsController := consoleapi.NewUsageLimits(logger, service)
219	router.Handle(
220		"/api/v0/projects/{id}/usage-limits",
221		server.withAuth(http.HandlerFunc(usageLimitsController.ProjectUsageLimits)),
222	).Methods(http.MethodGet)
223	router.Handle(
224		"/api/v0/projects/usage-limits",
225		server.withAuth(http.HandlerFunc(usageLimitsController.TotalUsageLimits)),
226	).Methods(http.MethodGet)
227
228	authController := consoleapi.NewAuth(logger, service, mailService, server.cookieAuth, partners, server.analytics, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL)
229	authRouter := router.PathPrefix("/api/v0/auth").Subrouter()
230	authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.GetAccount))).Methods(http.MethodGet)
231	authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.UpdateAccount))).Methods(http.MethodPatch)
232	authRouter.Handle("/account/change-email", server.withAuth(http.HandlerFunc(authController.ChangeEmail))).Methods(http.MethodPost)
233	authRouter.Handle("/account/change-password", server.withAuth(http.HandlerFunc(authController.ChangePassword))).Methods(http.MethodPost)
234	authRouter.Handle("/account/delete", server.withAuth(http.HandlerFunc(authController.DeleteAccount))).Methods(http.MethodPost)
235	authRouter.Handle("/mfa/enable", server.withAuth(http.HandlerFunc(authController.EnableUserMFA))).Methods(http.MethodPost)
236	authRouter.Handle("/mfa/disable", server.withAuth(http.HandlerFunc(authController.DisableUserMFA))).Methods(http.MethodPost)
237	authRouter.Handle("/mfa/generate-secret-key", server.withAuth(http.HandlerFunc(authController.GenerateMFASecretKey))).Methods(http.MethodPost)
238	authRouter.Handle("/mfa/generate-recovery-codes", server.withAuth(http.HandlerFunc(authController.GenerateMFARecoveryCodes))).Methods(http.MethodPost)
239	authRouter.HandleFunc("/logout", authController.Logout).Methods(http.MethodPost)
240	authRouter.Handle("/token", server.ipRateLimiter.Limit(http.HandlerFunc(authController.Token))).Methods(http.MethodPost)
241	authRouter.Handle("/register", server.ipRateLimiter.Limit(http.HandlerFunc(authController.Register))).Methods(http.MethodPost, http.MethodOptions)
242	authRouter.Handle("/forgot-password/{email}", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ForgotPassword))).Methods(http.MethodPost)
243	authRouter.Handle("/resend-email/{id}", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ResendEmail))).Methods(http.MethodPost)
244	authRouter.Handle("/reset-password", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ResetPassword))).Methods(http.MethodPost)
245
246	paymentController := consoleapi.NewPayments(logger, service)
247	paymentsRouter := router.PathPrefix("/api/v0/payments").Subrouter()
248	paymentsRouter.Use(server.withAuth)
249	paymentsRouter.HandleFunc("/cards", paymentController.AddCreditCard).Methods(http.MethodPost)
250	paymentsRouter.HandleFunc("/cards", paymentController.MakeCreditCardDefault).Methods(http.MethodPatch)
251	paymentsRouter.HandleFunc("/cards", paymentController.ListCreditCards).Methods(http.MethodGet)
252	paymentsRouter.HandleFunc("/cards/{cardId}", paymentController.RemoveCreditCard).Methods(http.MethodDelete)
253	paymentsRouter.HandleFunc("/account/charges", paymentController.ProjectsCharges).Methods(http.MethodGet)
254	paymentsRouter.HandleFunc("/account/balance", paymentController.AccountBalance).Methods(http.MethodGet)
255	paymentsRouter.HandleFunc("/account", paymentController.SetupAccount).Methods(http.MethodPost)
256	paymentsRouter.HandleFunc("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet)
257	paymentsRouter.HandleFunc("/tokens/deposit", paymentController.TokenDeposit).Methods(http.MethodPost)
258	paymentsRouter.Handle("/coupon/apply", server.userIDRateLimiter.Limit(http.HandlerFunc(paymentController.ApplyCouponCode))).Methods(http.MethodPatch)
259	paymentsRouter.HandleFunc("/coupon", paymentController.GetCoupon).Methods(http.MethodGet)
260
261	bucketsController := consoleapi.NewBuckets(logger, service)
262	bucketsRouter := router.PathPrefix("/api/v0/buckets").Subrouter()
263	bucketsRouter.Use(server.withAuth)
264	bucketsRouter.HandleFunc("/bucket-names", bucketsController.AllBucketNames).Methods(http.MethodGet)
265
266	apiKeysController := consoleapi.NewAPIKeys(logger, service)
267	apiKeysRouter := router.PathPrefix("/api/v0/api-keys").Subrouter()
268	apiKeysRouter.Use(server.withAuth)
269	apiKeysRouter.HandleFunc("/delete-by-name", apiKeysController.DeleteByNameAndProjectID).Methods(http.MethodDelete)
270
271	analyticsController := consoleapi.NewAnalytics(logger, service, server.analytics)
272	analyticsRouter := router.PathPrefix("/api/v0/analytics").Subrouter()
273	analyticsRouter.Use(server.withAuth)
274	analyticsRouter.HandleFunc("/event", analyticsController.EventTriggered).Methods(http.MethodPost)
275
276	if server.config.StaticDir != "" {
277		router.HandleFunc("/activation/", server.accountActivationHandler)
278		router.HandleFunc("/cancel-password-recovery/", server.cancelPasswordRecoveryHandler)
279		router.HandleFunc("/usage-report", server.bucketUsageReportHandler)
280		router.PathPrefix("/static/").Handler(server.brotliMiddleware(http.StripPrefix("/static", fs)))
281		router.PathPrefix("/").Handler(http.HandlerFunc(server.appHandler))
282	}
283
284	server.server = http.Server{
285		Handler:        server.withRequest(router),
286		MaxHeaderBytes: ContentLengthLimit.Int(),
287	}
288
289	return &server
290}
291
292// Run starts the server that host webapp and api endpoint.
293func (server *Server) Run(ctx context.Context) (err error) {
294	defer mon.Task()(&ctx)(&err)
295
296	server.schema, err = consoleql.CreateSchema(server.log, server.service, server.mailService)
297	if err != nil {
298		return Error.Wrap(err)
299	}
300
301	_, err = server.loadTemplates()
302	if err != nil {
303		// TODO: should it return error if some template can not be initialized or just log about it?
304		return Error.Wrap(err)
305	}
306
307	ctx, cancel := context.WithCancel(ctx)
308	var group errgroup.Group
309	group.Go(func() error {
310		<-ctx.Done()
311		return server.server.Shutdown(context.Background())
312	})
313	group.Go(func() error {
314		server.ipRateLimiter.Run(ctx)
315		return nil
316	})
317	group.Go(func() error {
318		defer cancel()
319		err := server.server.Serve(server.listener)
320		if errs2.IsCanceled(err) || errors.Is(err, http.ErrServerClosed) {
321			err = nil
322		}
323		return err
324	})
325
326	return group.Wait()
327}
328
329// Close closes server and underlying listener.
330func (server *Server) Close() error {
331	return server.server.Close()
332}
333
334// appHandler is web app http handler function.
335func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
336	header := w.Header()
337
338	if server.config.CSPEnabled {
339		cspValues := []string{
340			"default-src 'self'",
341			"connect-src 'self' *.tardigradeshare.io *.storjshare.io " + server.config.GatewayCredentialsRequestURL,
342			"frame-ancestors " + server.config.FrameAncestors,
343			"frame-src 'self' *.stripe.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/",
344			"img-src 'self' data: *.tardigradeshare.io *.storjshare.io",
345			"media-src 'self' *.tardigradeshare.io *.storjshare.io",
346			"script-src 'sha256-wAqYV6m2PHGd1WDyFBnZmSoyfCK0jxFAns0vGbdiWUA=' 'self' *.stripe.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/",
347		}
348
349		header.Set("Content-Security-Policy", strings.Join(cspValues, "; "))
350	}
351
352	header.Set(contentType, "text/html; charset=UTF-8")
353	header.Set("X-Content-Type-Options", "nosniff")
354	header.Set("Referrer-Policy", "same-origin") // Only expose the referring url when navigating around the satellite itself.
355
356	var data struct {
357		ExternalAddress                 string
358		SatelliteName                   string
359		SatelliteNodeURL                string
360		StripePublicKey                 string
361		PartneredSatellites             string
362		DefaultProjectLimit             int
363		GeneralRequestURL               string
364		ProjectLimitsIncreaseRequestURL string
365		GatewayCredentialsRequestURL    string
366		IsBetaSatellite                 bool
367		BetaSatelliteFeedbackURL        string
368		BetaSatelliteSupportURL         string
369		DocumentationURL                string
370		CouponCodeBillingUIEnabled      bool
371		CouponCodeSignupUIEnabled       bool
372		FileBrowserFlowDisabled         bool
373		LinksharingURL                  string
374		PathwayOverviewEnabled          bool
375		StorageTBPrice                  string
376		EgressTBPrice                   string
377		SegmentPrice                    string
378		RecaptchaEnabled                bool
379		RecaptchaSiteKey                string
380		NewProjectDashboard             bool
381		NewOnboarding                   bool
382		DefaultPaidStorageLimit         memory.Size
383		DefaultPaidBandwidthLimit       memory.Size
384		NewNavigation                   bool
385		NewBrowser                      bool
386		NewObjectsFlow                  bool
387	}
388
389	data.ExternalAddress = server.config.ExternalAddress
390	data.SatelliteName = server.config.SatelliteName
391	data.SatelliteNodeURL = server.nodeURL.String()
392	data.StripePublicKey = server.stripePublicKey
393	data.PartneredSatellites = string(server.config.PartneredSatellites)
394	data.DefaultProjectLimit = server.config.DefaultProjectLimit
395	data.GeneralRequestURL = server.config.GeneralRequestURL
396	data.ProjectLimitsIncreaseRequestURL = server.config.ProjectLimitsIncreaseRequestURL
397	data.GatewayCredentialsRequestURL = server.config.GatewayCredentialsRequestURL
398	data.IsBetaSatellite = server.config.IsBetaSatellite
399	data.BetaSatelliteFeedbackURL = server.config.BetaSatelliteFeedbackURL
400	data.BetaSatelliteSupportURL = server.config.BetaSatelliteSupportURL
401	data.DocumentationURL = server.config.DocumentationURL
402	data.CouponCodeBillingUIEnabled = server.config.CouponCodeBillingUIEnabled
403	data.CouponCodeSignupUIEnabled = server.config.CouponCodeSignupUIEnabled
404	data.FileBrowserFlowDisabled = server.config.FileBrowserFlowDisabled
405	data.LinksharingURL = server.config.LinksharingURL
406	data.PathwayOverviewEnabled = server.config.PathwayOverviewEnabled
407	data.DefaultPaidStorageLimit = server.config.UsageLimits.Storage.Paid
408	data.DefaultPaidBandwidthLimit = server.config.UsageLimits.Bandwidth.Paid
409	data.StorageTBPrice = server.pricing.StorageTBPrice
410	data.EgressTBPrice = server.pricing.EgressTBPrice
411	data.SegmentPrice = server.pricing.SegmentPrice
412	data.RecaptchaEnabled = server.config.Recaptcha.Enabled
413	data.RecaptchaSiteKey = server.config.Recaptcha.SiteKey
414	data.NewProjectDashboard = server.config.NewProjectDashboard
415	data.NewOnboarding = server.config.NewOnboarding
416	data.NewNavigation = server.config.NewNavigation
417	data.NewBrowser = server.config.NewBrowser
418	data.NewObjectsFlow = server.config.NewObjectsFlow
419
420	templates, err := server.loadTemplates()
421	if err != nil || templates.index == nil {
422		server.log.Error("unable to load templates", zap.Error(err))
423		fmt.Fprintf(w, "Unable to load templates. See whether satellite UI has been built.")
424		return
425	}
426
427	if err := templates.index.Execute(w, data); err != nil {
428		server.log.Error("index template could not be executed", zap.Error(err))
429		return
430	}
431}
432
433// authMiddlewareHandler performs initial authorization before every request.
434func (server *Server) withAuth(handler http.Handler) http.Handler {
435	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
436		var err error
437		var ctx context.Context
438
439		defer mon.Task()(&ctx)(&err)
440
441		ctxWithAuth := func(ctx context.Context) context.Context {
442			token, err := server.cookieAuth.GetToken(r)
443			if err != nil {
444				return console.WithAuthFailure(ctx, err)
445			}
446
447			ctx = consoleauth.WithAPIKey(ctx, []byte(token))
448
449			auth, err := server.service.Authorize(ctx)
450			if err != nil {
451				return console.WithAuthFailure(ctx, err)
452			}
453
454			return console.WithAuth(ctx, auth)
455		}
456
457		ctx = ctxWithAuth(r.Context())
458
459		handler.ServeHTTP(w, r.Clone(ctx))
460	})
461}
462
463// withRequest ensures the http request itself is reachable from the context.
464func (server *Server) withRequest(handler http.Handler) http.Handler {
465	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
466		handler.ServeHTTP(w, r.Clone(console.WithRequest(r.Context(), r)))
467	})
468}
469
470// bucketUsageReportHandler generate bucket usage report page for project.
471func (server *Server) bucketUsageReportHandler(w http.ResponseWriter, r *http.Request) {
472	ctx := r.Context()
473	var err error
474	defer mon.Task()(&ctx)(&err)
475
476	token, err := server.cookieAuth.GetToken(r)
477	if err != nil {
478		server.serveError(w, http.StatusUnauthorized)
479		return
480	}
481
482	auth, err := server.service.Authorize(consoleauth.WithAPIKey(ctx, []byte(token)))
483	if err != nil {
484		server.serveError(w, http.StatusUnauthorized)
485		return
486	}
487
488	ctx = console.WithAuth(ctx, auth)
489
490	// parse query params
491	projectID, err := uuid.FromString(r.URL.Query().Get("projectID"))
492	if err != nil {
493		server.serveError(w, http.StatusBadRequest)
494		return
495	}
496	sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64)
497	if err != nil {
498		server.serveError(w, http.StatusBadRequest)
499		return
500	}
501	beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("before"), 10, 64)
502	if err != nil {
503		server.serveError(w, http.StatusBadRequest)
504		return
505	}
506
507	since := time.Unix(sinceStamp, 0).UTC()
508	before := time.Unix(beforeStamp, 0).UTC()
509
510	server.log.Debug("querying bucket usage report",
511		zap.Stringer("projectID", projectID),
512		zap.Stringer("since", since),
513		zap.Stringer("before", before))
514
515	bucketRollups, err := server.service.GetBucketUsageRollups(ctx, projectID, since, before)
516	if err != nil {
517		server.log.Error("bucket usage report error", zap.Error(err))
518		server.serveError(w, http.StatusInternalServerError)
519		return
520	}
521
522	templates, err := server.loadTemplates()
523	if err != nil {
524		server.log.Error("unable to load templates", zap.Error(err))
525		return
526	}
527	if err = templates.usageReport.Execute(w, bucketRollups); err != nil {
528		server.log.Error("bucket usage report error", zap.Error(err))
529	}
530}
531
532// createRegistrationTokenHandler is web app http handler function.
533func (server *Server) createRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) {
534	ctx := r.Context()
535	defer mon.Task()(&ctx)(nil)
536	w.Header().Set(contentType, applicationJSON)
537
538	var response struct {
539		Secret string `json:"secret"`
540		Error  string `json:"error,omitempty"`
541	}
542
543	defer func() {
544		err := json.NewEncoder(w).Encode(&response)
545		if err != nil {
546			server.log.Error(err.Error())
547		}
548	}()
549
550	equality := subtle.ConstantTimeCompare(
551		[]byte(r.Header.Get("Authorization")),
552		[]byte(server.config.AuthToken),
553	)
554	if equality != 1 {
555		w.WriteHeader(401)
556		response.Error = "unauthorized"
557		return
558	}
559
560	projectsLimitInput := r.URL.Query().Get("projectsLimit")
561
562	projectsLimit, err := strconv.Atoi(projectsLimitInput)
563	if err != nil {
564		response.Error = err.Error()
565		return
566	}
567
568	token, err := server.service.CreateRegToken(ctx, projectsLimit)
569	if err != nil {
570		response.Error = err.Error()
571		return
572	}
573
574	response.Secret = token.Secret.String()
575}
576
577// accountActivationHandler is web app http handler function.
578func (server *Server) accountActivationHandler(w http.ResponseWriter, r *http.Request) {
579	ctx := r.Context()
580	defer mon.Task()(&ctx)(nil)
581	activationToken := r.URL.Query().Get("token")
582
583	token, err := server.service.ActivateAccount(ctx, activationToken)
584	if err != nil {
585		server.log.Error("activation: failed to activate account",
586			zap.String("token", activationToken),
587			zap.Error(err))
588
589		if console.ErrEmailUsed.Has(err) {
590			http.Redirect(w, r, server.config.ExternalAddress+"login?activated=false", http.StatusTemporaryRedirect)
591			return
592		}
593
594		if console.Error.Has(err) {
595			server.serveError(w, http.StatusInternalServerError)
596			return
597		}
598
599		server.serveError(w, http.StatusNotFound)
600		return
601	}
602
603	server.cookieAuth.SetTokenCookie(w, token)
604
605	http.Redirect(w, r, server.config.ExternalAddress, http.StatusTemporaryRedirect)
606}
607
608func (server *Server) cancelPasswordRecoveryHandler(w http.ResponseWriter, r *http.Request) {
609	ctx := r.Context()
610	defer mon.Task()(&ctx)(nil)
611	recoveryToken := r.URL.Query().Get("token")
612
613	// No need to check error as we anyway redirect user to support page
614	_ = server.service.RevokeResetPasswordToken(ctx, recoveryToken)
615
616	// TODO: Should place this link to config
617	http.Redirect(w, r, "https://storjlabs.atlassian.net/servicedesk/customer/portals", http.StatusSeeOther)
618}
619
620// graphqlHandler is graphql endpoint http handler function.
621func (server *Server) graphqlHandler(w http.ResponseWriter, r *http.Request) {
622	ctx := r.Context()
623	defer mon.Task()(&ctx)(nil)
624
625	handleError := func(code int, err error) {
626		w.WriteHeader(code)
627
628		var jsonError struct {
629			Error string `json:"error"`
630		}
631
632		jsonError.Error = err.Error()
633
634		if err := json.NewEncoder(w).Encode(jsonError); err != nil {
635			server.log.Error("error graphql error", zap.Error(err))
636		}
637	}
638
639	w.Header().Set(contentType, applicationJSON)
640
641	query, err := getQuery(w, r)
642	if err != nil {
643		handleError(http.StatusBadRequest, err)
644		return
645	}
646
647	rootObject := make(map[string]interface{})
648
649	rootObject["origin"] = server.config.ExternalAddress
650	rootObject[consoleql.ActivationPath] = "activation/?token="
651	rootObject[consoleql.PasswordRecoveryPath] = "password-recovery/?token="
652	rootObject[consoleql.CancelPasswordRecoveryPath] = "cancel-password-recovery/?token="
653	rootObject[consoleql.SignInPath] = "login"
654	rootObject[consoleql.LetUsKnowURL] = server.config.LetUsKnowURL
655	rootObject[consoleql.ContactInfoURL] = server.config.ContactInfoURL
656	rootObject[consoleql.TermsAndConditionsURL] = server.config.TermsAndConditionsURL
657
658	result := graphql.Do(graphql.Params{
659		Schema:         server.schema,
660		Context:        ctx,
661		RequestString:  query.Query,
662		VariableValues: query.Variables,
663		OperationName:  query.OperationName,
664		RootObject:     rootObject,
665	})
666
667	getGqlError := func(err gqlerrors.FormattedError) error {
668		var gerr *gqlerrors.Error
669		if errors.As(err.OriginalError(), &gerr) {
670			return gerr.OriginalError
671		}
672		return nil
673	}
674
675	parseConsoleError := func(err error) (int, error) {
676		switch {
677		case console.ErrUnauthorized.Has(err):
678			return http.StatusUnauthorized, err
679		case console.Error.Has(err):
680			return http.StatusInternalServerError, err
681		}
682
683		return 0, nil
684	}
685
686	handleErrors := func(code int, errors gqlerrors.FormattedErrors) {
687		w.WriteHeader(code)
688
689		var jsonError struct {
690			Errors []string `json:"errors"`
691		}
692
693		for _, err := range errors {
694			jsonError.Errors = append(jsonError.Errors, err.Message)
695		}
696
697		if err := json.NewEncoder(w).Encode(jsonError); err != nil {
698			server.log.Error("error graphql error", zap.Error(err))
699		}
700	}
701
702	handleGraphqlErrors := func() {
703		for _, err := range result.Errors {
704			gqlErr := getGqlError(err)
705			if gqlErr == nil {
706				continue
707			}
708
709			code, err := parseConsoleError(gqlErr)
710			if err != nil {
711				handleError(code, err)
712				return
713			}
714		}
715
716		handleErrors(http.StatusOK, result.Errors)
717	}
718
719	if result.HasErrors() {
720		handleGraphqlErrors()
721		return
722	}
723
724	err = json.NewEncoder(w).Encode(result)
725	if err != nil {
726		server.log.Error("error encoding grapql result", zap.Error(err))
727		return
728	}
729
730	server.log.Debug(fmt.Sprintf("%s", result))
731}
732
733// serveError serves error static pages.
734func (server *Server) serveError(w http.ResponseWriter, status int) {
735	w.WriteHeader(status)
736
737	switch status {
738	case http.StatusInternalServerError:
739		templates, err := server.loadTemplates()
740		if err != nil {
741			server.log.Error("unable to load templates", zap.Error(err))
742			return
743		}
744		err = templates.internalServerError.Execute(w, nil)
745		if err != nil {
746			server.log.Error("cannot parse internalServerError template", zap.Error(err))
747		}
748	case http.StatusNotFound:
749		templates, err := server.loadTemplates()
750		if err != nil {
751			server.log.Error("unable to load templates", zap.Error(err))
752			return
753		}
754		err = templates.notFound.Execute(w, nil)
755		if err != nil {
756			server.log.Error("cannot parse pageNotFound template", zap.Error(err))
757		}
758	}
759}
760
761// seoHandler used to communicate with web crawlers and other web robots.
762func (server *Server) seoHandler(w http.ResponseWriter, req *http.Request) {
763	header := w.Header()
764
765	header.Set(contentType, mime.TypeByExtension(".txt"))
766	header.Set("X-Content-Type-Options", "nosniff")
767
768	_, err := w.Write([]byte(server.config.SEO))
769	if err != nil {
770		server.log.Error(err.Error())
771	}
772}
773
774// brotliMiddleware is used to compress static content using brotli to minify resources if browser support such decoding.
775func (server *Server) brotliMiddleware(fn http.Handler) http.Handler {
776	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
777		w.Header().Set("Cache-Control", "public, max-age=31536000")
778		w.Header().Set("X-Content-Type-Options", "nosniff")
779
780		isBrotliSupported := strings.Contains(r.Header.Get("Accept-Encoding"), "br")
781		if !isBrotliSupported {
782			fn.ServeHTTP(w, r)
783			return
784		}
785
786		info, err := os.Stat(server.config.StaticDir + strings.TrimPrefix(r.URL.Path, "/static") + ".br")
787		if err != nil {
788			fn.ServeHTTP(w, r)
789			return
790		}
791
792		extension := filepath.Ext(info.Name()[:len(info.Name())-3])
793		w.Header().Set(contentType, mime.TypeByExtension(extension))
794		w.Header().Set("Content-Encoding", "br")
795
796		newRequest := new(http.Request)
797		*newRequest = *r
798		newRequest.URL = new(url.URL)
799		*newRequest.URL = *r.URL
800		newRequest.URL.Path += ".br"
801
802		fn.ServeHTTP(w, newRequest)
803	})
804}
805
806// loadTemplates is used to initialize all templates.
807func (server *Server) loadTemplates() (_ *templates, err error) {
808	if server.config.Watch {
809		return server.parseTemplates()
810	}
811
812	if server.templatesCache != nil {
813		return server.templatesCache, nil
814	}
815
816	templates, err := server.parseTemplates()
817	if err != nil {
818		return nil, Error.Wrap(err)
819	}
820
821	server.templatesCache = templates
822	return server.templatesCache, nil
823}
824
825func (server *Server) parseTemplates() (_ *templates, err error) {
826	var t templates
827
828	t.index, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "dist", "index.html"))
829	if err != nil {
830		server.log.Error("dist folder is not generated. use 'npm run build' command", zap.Error(err))
831		// Loading index is optional.
832	}
833
834	t.usageReport, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "reports", "usageReport.html"))
835	if err != nil {
836		return &t, Error.Wrap(err)
837	}
838
839	t.notFound, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "errors", "404.html"))
840	if err != nil {
841		return &t, Error.Wrap(err)
842	}
843
844	t.internalServerError, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "errors", "500.html"))
845	if err != nil {
846		return &t, Error.Wrap(err)
847	}
848
849	return &t, nil
850}
851
852// NewUserIDRateLimiter constructs a RateLimiter that limits based on user ID.
853func NewUserIDRateLimiter(config web.RateLimiterConfig) *web.RateLimiter {
854	return web.NewRateLimiter(config, func(r *http.Request) (string, error) {
855		auth, err := console.GetAuth(r.Context())
856		if err != nil {
857			return "", err
858		}
859		return auth.User.ID.String(), nil
860	})
861}
862