2019-01-24 16:26:36 +00:00
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleweb
import (
"context"
2020-01-14 13:38:32 +00:00
"crypto/subtle"
2019-01-24 16:26:36 +00:00
"encoding/json"
2020-04-16 16:50:22 +01:00
"errors"
2020-04-13 10:31:17 +01:00
"fmt"
2019-04-10 00:14:19 +01:00
"html/template"
2019-08-08 13:12:39 +01:00
"mime"
2019-01-24 16:26:36 +00:00
"net"
"net/http"
2019-08-08 13:12:39 +01:00
"net/url"
2019-11-12 13:05:35 +00:00
"os"
2019-04-10 00:14:19 +01:00
"path"
2019-01-24 16:26:36 +00:00
"path/filepath"
2019-03-19 17:55:43 +00:00
"strconv"
2019-03-26 15:56:16 +00:00
"strings"
2019-04-10 00:14:19 +01:00
"time"
2019-01-24 16:26:36 +00:00
2019-10-21 17:42:49 +01:00
"github.com/gorilla/mux"
2019-01-24 16:26:36 +00:00
"github.com/graphql-go/graphql"
2020-01-20 13:02:44 +00:00
"github.com/graphql-go/graphql/gqlerrors"
2020-10-09 14:40:12 +01:00
"github.com/spacemonkeygo/monkit/v3"
2019-01-24 16:26:36 +00:00
"github.com/zeebo/errs"
"go.uber.org/zap"
2019-02-06 13:19:14 +00:00
"golang.org/x/sync/errgroup"
2019-01-24 16:26:36 +00:00
2020-04-16 16:50:22 +01:00
"storj.io/common/errs2"
2020-12-23 04:24:37 +00:00
"storj.io/common/storj"
2020-03-30 10:08:50 +01:00
"storj.io/common/uuid"
2020-04-08 20:40:49 +01:00
"storj.io/storj/private/web"
2020-12-22 12:05:22 +00:00
"storj.io/storj/satellite/accounting"
2019-01-24 16:26:36 +00:00
"storj.io/storj/satellite/console"
2020-10-06 11:40:31 +01:00
"storj.io/storj/satellite/console/consoleauth"
2019-10-17 15:42:18 +01:00
"storj.io/storj/satellite/console/consoleweb/consoleapi"
2019-01-24 16:26:36 +00:00
"storj.io/storj/satellite/console/consoleweb/consoleql"
2020-01-20 18:57:14 +00:00
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
2019-03-02 15:22:20 +00:00
"storj.io/storj/satellite/mailservice"
2019-11-25 21:36:36 +00:00
"storj.io/storj/satellite/referrals"
2020-07-28 15:23:17 +01:00
"storj.io/storj/satellite/rewards"
2019-01-24 16:26:36 +00:00
)
const (
2020-01-20 18:57:14 +00:00
contentType = "Content-Type"
2019-01-24 16:26:36 +00:00
applicationJSON = "application/json"
applicationGraphql = "application/graphql"
)
2019-06-04 12:55:38 +01:00
var (
2020-08-11 15:50:01 +01:00
// Error is satellite console error type.
2019-06-04 12:55:38 +01:00
Error = errs . Class ( "satellite console error" )
mon = monkit . Package ( )
)
2019-01-24 16:26:36 +00:00
2020-07-16 15:18:02 +01:00
// Config contains configuration for console web server.
2019-01-24 16:26:36 +00:00
type Config struct {
2021-01-05 11:13:27 +00:00
Address string ` help:"server address of the graphql api gateway and frontend app" devDefault:"" releaseDefault:":10100" `
2019-03-26 15:56:16 +00:00
StaticDir string ` help:"path to static resources" default:"" `
ExternalAddress string ` help:"external endpoint of the satellite if hosted" default:"" `
2019-02-05 17:31:53 +00:00
2019-03-19 17:55:43 +00:00
// TODO: remove after Vanguard release
2019-05-28 15:32:51 +01:00
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" `
2019-03-19 17:55:43 +00:00
2020-09-04 14:58:50 +01:00
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:"" `
GeneralRequestURL string ` help:"url link to general request page" default:"https://support.tardigrade.io/hc/en-us/requests/new?ticket_form_id=360000379291" `
ProjectLimitsIncreaseRequestURL string ` help:"url link to project limit increase request page" default:"https://support.tardigrade.io/hc/en-us/requests/new?ticket_form_id=360000683212" `
2020-12-17 22:38:16 +00:00
GatewayCredentialsRequestURL string ` help:"url link for gateway credentials requests" default:"https://auth.tardigradeshare.io" `
2020-03-11 15:36:55 +00:00
2020-04-08 20:40:49 +01:00
RateLimit web . IPRateLimiterConfig
2020-03-11 15:36:55 +00:00
console . Config
2019-01-24 16:26:36 +00:00
}
2020-12-05 16:01:42 +00:00
// Server represents console web server.
2019-09-10 14:24:16 +01:00
//
// architecture: Endpoint
2019-01-24 16:26:36 +00:00
type Server struct {
log * zap . Logger
2019-11-25 21:36:36 +00:00
config Config
service * console . Service
mailService * mailservice . Service
referralsService * referrals . Service
2020-07-28 15:23:17 +01:00
partners * rewards . PartnersService
2019-03-02 15:22:20 +00:00
2020-04-08 20:40:49 +01:00
listener net . Listener
server http . Server
cookieAuth * consolewebauth . CookieAuth
rateLimiter * web . IPRateLimiter
2020-12-23 04:24:37 +00:00
nodeURL storj . NodeURL
2019-01-24 16:26:36 +00:00
2019-11-18 11:38:43 +00:00
stripePublicKey string
2019-08-13 13:37:01 +01:00
schema graphql . Schema
templates struct {
2019-10-31 18:42:28 +00:00
index * template . Template
notFound * template . Template
internalServerError * template . Template
usageReport * template . Template
resetPassword * template . Template
success * template . Template
activated * template . Template
2019-08-13 13:37:01 +01:00
}
2019-01-24 16:26:36 +00:00
}
2019-10-17 15:42:18 +01:00
// NewServer creates new instance of console server.
2020-12-23 04:24:37 +00:00
func NewServer ( logger * zap . Logger , config Config , service * console . Service , mailService * mailservice . Service , referralsService * referrals . Service , partners * rewards . PartnersService , listener net . Listener , stripePublicKey string , nodeURL storj . NodeURL ) * Server {
2019-01-24 16:26:36 +00:00
server := Server {
2019-11-25 21:36:36 +00:00
log : logger ,
config : config ,
listener : listener ,
service : service ,
mailService : mailService ,
referralsService : referralsService ,
2020-07-28 15:23:17 +01:00
partners : partners ,
2019-11-25 21:36:36 +00:00
stripePublicKey : stripePublicKey ,
2020-04-08 20:40:49 +01:00
rateLimiter : web . NewIPRateLimiter ( config . RateLimit ) ,
2020-12-23 04:24:37 +00:00
nodeURL : nodeURL ,
2019-01-24 16:26:36 +00:00
}
2020-04-13 10:31:17 +01:00
logger . Debug ( "Starting Satellite UI." , zap . Stringer ( "Address" , server . listener . Addr ( ) ) )
2019-02-28 20:12:52 +00:00
2020-01-20 18:57:14 +00:00
server . cookieAuth = consolewebauth . NewCookieAuth ( consolewebauth . CookieSettings {
Name : "_tokenKey" ,
Path : "/" ,
} )
2019-03-26 15:56:16 +00:00
if server . config . ExternalAddress != "" {
if ! strings . HasSuffix ( server . config . ExternalAddress , "/" ) {
2019-05-29 14:14:25 +01:00
server . config . ExternalAddress += "/"
2019-03-26 15:56:16 +00:00
}
} else {
server . config . ExternalAddress = "http://" + server . listener . Addr ( ) . String ( ) + "/"
}
2020-03-23 20:38:50 +00:00
if server . config . AccountActivationRedirectURL == "" {
server . config . AccountActivationRedirectURL = server . config . ExternalAddress + "login?activated=true"
}
2019-10-21 17:42:49 +01:00
router := mux . NewRouter ( )
2019-01-24 16:26:36 +00:00
fs := http . FileServer ( http . Dir ( server . config . StaticDir ) )
2019-10-22 17:17:09 +01:00
2019-10-21 17:42:49 +01:00
router . HandleFunc ( "/registrationToken/" , server . createRegistrationTokenHandler )
2020-01-18 02:34:06 +00:00
router . HandleFunc ( "/populate-promotional-coupons" , server . populatePromotionalCoupons ) . Methods ( http . MethodPost )
2019-10-21 17:42:49 +01:00
router . HandleFunc ( "/robots.txt" , server . seoHandler )
2020-10-21 10:58:37 +01:00
router . Handle ( "/api/v0/graphql" , server . withAuth ( http . HandlerFunc ( server . graphqlHandler ) ) )
2020-01-20 18:57:14 +00:00
2019-12-12 12:58:15 +00:00
router . Handle (
"/api/v0/projects/{id}/usage-limits" ,
server . withAuth ( http . HandlerFunc ( server . projectUsageLimitsHandler ) ) ,
) . Methods ( http . MethodGet )
2019-11-25 21:36:36 +00:00
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 )
2020-07-28 15:23:17 +01:00
authController := consoleapi . NewAuth ( logger , service , mailService , server . cookieAuth , partners , server . config . ExternalAddress , config . LetUsKnowURL , config . TermsAndConditionsURL , config . ContactInfoURL )
2019-10-23 18:33:24 +01:00
authRouter := router . PathPrefix ( "/api/v0/auth" ) . Subrouter ( )
2019-10-29 14:24:16 +00:00
authRouter . Handle ( "/account" , server . withAuth ( http . HandlerFunc ( authController . GetAccount ) ) ) . Methods ( http . MethodGet )
authRouter . Handle ( "/account" , server . withAuth ( http . HandlerFunc ( authController . UpdateAccount ) ) ) . Methods ( http . MethodPatch )
2020-11-05 16:16:55 +00:00
authRouter . Handle ( "/account/change-email" , server . withAuth ( http . HandlerFunc ( authController . ChangeEmail ) ) ) . Methods ( http . MethodPost )
2019-10-29 14:24:16 +00:00
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 )
2020-02-21 11:47:53 +00:00
authRouter . HandleFunc ( "/logout" , authController . Logout ) . Methods ( http . MethodPost )
2020-04-08 20:40:49 +01:00
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 )
2019-10-21 13:48:29 +01:00
2019-10-23 18:33:24 +01:00
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 )
2019-11-15 14:27:44 +00:00
paymentsRouter . HandleFunc ( "/account/charges" , paymentController . ProjectsCharges ) . Methods ( http . MethodGet )
2019-10-23 18:33:24 +01:00
paymentsRouter . HandleFunc ( "/account/balance" , paymentController . AccountBalance ) . Methods ( http . MethodGet )
paymentsRouter . HandleFunc ( "/account" , paymentController . SetupAccount ) . Methods ( http . MethodPost )
2019-10-31 16:56:54 +00:00
paymentsRouter . HandleFunc ( "/billing-history" , paymentController . BillingHistory ) . Methods ( http . MethodGet )
2019-11-12 11:14:34 +00:00
paymentsRouter . HandleFunc ( "/tokens/deposit" , paymentController . TokenDeposit ) . Methods ( http . MethodPost )
2020-07-10 14:05:17 +01:00
paymentsRouter . HandleFunc ( "/paywall-enabled/{userId}" , paymentController . PaywallEnabled ) . Methods ( http . MethodGet )
2019-01-24 16:26:36 +00:00
2020-11-13 11:41:35 +00:00
bucketsController := consoleapi . NewBuckets ( logger , service )
bucketsRouter := router . PathPrefix ( "/api/v0/buckets" ) . Subrouter ( )
bucketsRouter . Use ( server . withAuth )
bucketsRouter . HandleFunc ( "/bucket-names" , bucketsController . AllBucketNames ) . Methods ( http . MethodGet )
2019-01-24 16:26:36 +00:00
if server . config . StaticDir != "" {
2019-10-21 17:42:49 +01:00
router . HandleFunc ( "/activation/" , server . accountActivationHandler )
router . HandleFunc ( "/password-recovery/" , server . passwordRecoveryHandler )
router . HandleFunc ( "/cancel-password-recovery/" , server . cancelPasswordRecoveryHandler )
2019-11-22 17:03:15 +00:00
router . HandleFunc ( "/usage-report" , server . bucketUsageReportHandler )
2020-12-15 19:06:26 +00:00
router . PathPrefix ( "/static/" ) . Handler ( server . brotliMiddleware ( http . StripPrefix ( "/static" , fs ) ) )
2019-10-21 17:42:49 +01:00
router . PathPrefix ( "/" ) . Handler ( http . HandlerFunc ( server . appHandler ) )
2019-01-24 16:26:36 +00:00
}
server . server = http . Server {
2020-09-06 02:56:07 +01:00
Handler : server . withRequest ( router ) ,
2019-09-20 18:40:26 +01:00
MaxHeaderBytes : ContentLengthLimit . Int ( ) ,
2019-01-24 16:26:36 +00:00
}
return & server
}
2020-01-24 13:38:53 +00:00
// Run starts the server that host webapp and api endpoint.
2019-08-13 13:37:01 +01:00
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 ( )
2019-08-22 12:40:15 +01:00
return server . server . Shutdown ( context . Background ( ) )
2019-08-13 13:37:01 +01:00
} )
group . Go ( func ( ) error {
2020-04-08 20:40:49 +01:00
server . rateLimiter . Run ( ctx )
return nil
} )
group . Go ( func ( ) error {
2019-08-13 13:37:01 +01:00
defer cancel ( )
2020-04-16 16:50:22 +01:00
err := server . server . Serve ( server . listener )
if errs2 . IsCanceled ( err ) || errors . Is ( err , http . ErrServerClosed ) {
err = nil
}
return err
2019-08-13 13:37:01 +01:00
} )
return group . Wait ( )
}
2020-07-16 15:18:02 +01:00
// Close closes server and underlying listener.
2019-08-13 13:37:01 +01:00
func ( server * Server ) Close ( ) error {
return server . server . Close ( )
}
2020-07-16 15:18:02 +01:00
// appHandler is web app http handler function.
2019-08-13 13:37:01 +01:00
func ( server * Server ) appHandler ( w http . ResponseWriter , r * http . Request ) {
2019-07-30 11:13:24 +01:00
header := w . Header ( )
cspValues := [ ] string {
"default-src 'self'" ,
2020-11-26 13:03:32 +00:00
"connect-src 'self' api.segment.io *.google-analytics.com " + server . config . GatewayCredentialsRequestURL ,
2019-11-21 16:15:22 +00:00
"frame-ancestors " + server . config . FrameAncestors ,
2020-06-11 18:57:32 +01:00
"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" ,
2019-07-30 11:13:24 +01:00
}
2019-09-09 19:33:05 +01:00
header . Set ( contentType , "text/html; charset=UTF-8" )
2019-07-30 11:13:24 +01:00
header . Set ( "Content-Security-Policy" , strings . Join ( cspValues , "; " ) )
2019-09-09 19:33:05 +01:00
header . Set ( "X-Content-Type-Options" , "nosniff" )
2019-11-21 16:15:22 +00:00
header . Set ( "Referrer-Policy" , "same-origin" ) // Only expose the referring url when navigating around the satellite itself.
2019-07-30 11:13:24 +01:00
2019-11-06 12:27:26 +00:00
var data struct {
2020-09-04 14:58:50 +01:00
SatelliteName string
2020-12-23 04:24:37 +00:00
SatelliteNodeURL string
2020-09-04 14:58:50 +01:00
SegmentIOPublicKey string
StripePublicKey string
VerificationPageURL string
PartneredSatelliteNames string
GoogleTagManagerID string
DefaultProjectLimit int
GeneralRequestURL string
ProjectLimitsIncreaseRequestURL string
2020-11-24 22:29:19 +00:00
GatewayCredentialsRequestURL string
2019-11-06 12:27:26 +00:00
}
data . SatelliteName = server . config . SatelliteName
2020-12-23 04:24:37 +00:00
data . SatelliteNodeURL = server . nodeURL . String ( )
2019-11-27 16:57:59 +00:00
data . SegmentIOPublicKey = server . config . SegmentIOPublicKey
2019-11-18 11:38:43 +00:00
data . StripePublicKey = server . stripePublicKey
2020-04-15 12:36:21 +01:00
data . VerificationPageURL = server . config . VerificationPageURL
2020-05-29 16:36:59 +01:00
data . PartneredSatelliteNames = server . config . PartneredSatelliteNames
2020-06-11 18:57:32 +01:00
data . GoogleTagManagerID = server . config . GoogleTagManagerID
2020-07-15 17:49:37 +01:00
data . DefaultProjectLimit = server . config . DefaultProjectLimit
2020-09-04 14:58:50 +01:00
data . GeneralRequestURL = server . config . GeneralRequestURL
data . ProjectLimitsIncreaseRequestURL = server . config . ProjectLimitsIncreaseRequestURL
2020-11-24 22:29:19 +00:00
data . GatewayCredentialsRequestURL = server . config . GatewayCredentialsRequestURL
2019-11-06 12:27:26 +00:00
2020-09-16 17:01:09 +01:00
if server . templates . index == nil {
server . log . Error ( "index template is not set" )
return
}
if err := server . templates . index . Execute ( w , data ) ; err != nil {
server . log . Error ( "index template could not be executed" , zap . Error ( err ) )
2019-08-13 13:37:01 +01:00
return
}
2019-01-24 16:26:36 +00:00
}
2019-10-21 17:42:49 +01:00
// 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
2020-01-20 18:57:14 +00:00
var ctx context . Context
2019-10-21 17:42:49 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2020-01-20 18:57:14 +00:00
ctxWithAuth := func ( ctx context . Context ) context . Context {
token , err := server . cookieAuth . GetToken ( r )
if err != nil {
return console . WithAuthFailure ( ctx , err )
}
2020-10-06 11:40:31 +01:00
ctx = consoleauth . WithAPIKey ( ctx , [ ] byte ( token ) )
2020-01-20 18:57:14 +00:00
auth , err := server . service . Authorize ( ctx )
if err != nil {
return console . WithAuthFailure ( ctx , err )
}
return console . WithAuth ( ctx , auth )
2019-10-21 17:42:49 +01:00
}
2020-01-20 18:57:14 +00:00
ctx = ctxWithAuth ( r . Context ( ) )
2019-10-21 17:42:49 +01:00
handler . ServeHTTP ( w , r . Clone ( ctx ) )
} )
}
2020-09-06 02:56:07 +01:00
// withRequest ensures the http request itself is reachable from the context.
func ( server * Server ) withRequest ( handler http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
handler . ServeHTTP ( w , r . Clone ( console . WithRequest ( r . Context ( ) , r ) ) )
} )
}
2020-01-24 13:38:53 +00:00
// bucketUsageReportHandler generate bucket usage report page for project.
2019-08-08 13:12:39 +01:00
func ( server * Server ) bucketUsageReportHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
2019-04-10 00:14:19 +01:00
var err error
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2019-04-10 00:14:19 +01:00
2020-01-20 18:57:14 +00:00
token , err := server . cookieAuth . GetToken ( r )
2019-09-13 16:38:29 +01:00
if err != nil {
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusUnauthorized )
2019-04-10 00:14:19 +01:00
return
}
2020-10-06 11:40:31 +01:00
auth , err := server . service . Authorize ( consoleauth . WithAPIKey ( ctx , [ ] byte ( token ) ) )
2019-04-10 00:14:19 +01:00
if err != nil {
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusUnauthorized )
2019-04-10 00:14:19 +01:00
return
}
2019-09-13 16:38:29 +01:00
ctx = console . WithAuth ( ctx , auth )
2019-04-10 00:14:19 +01:00
// parse query params
2020-04-02 13:30:43 +01:00
projectID , err := uuid . FromString ( r . URL . Query ( ) . Get ( "projectID" ) )
2019-04-10 00:14:19 +01:00
if err != nil {
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusBadRequest )
2019-04-10 00:14:19 +01:00
return
}
2019-08-08 13:12:39 +01:00
sinceStamp , err := strconv . ParseInt ( r . URL . Query ( ) . Get ( "since" ) , 10 , 64 )
2019-04-10 00:14:19 +01:00
if err != nil {
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusBadRequest )
2019-04-10 00:14:19 +01:00
return
}
2019-08-08 13:12:39 +01:00
beforeStamp , err := strconv . ParseInt ( r . URL . Query ( ) . Get ( "before" ) , 10 , 64 )
2019-04-10 00:14:19 +01:00
if err != nil {
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusBadRequest )
2019-04-10 00:14:19 +01:00
return
}
2020-01-02 12:52:33 +00:00
since := time . Unix ( sinceStamp , 0 ) . UTC ( )
before := time . Unix ( beforeStamp , 0 ) . UTC ( )
2019-04-23 13:56:15 +01:00
2019-08-08 13:12:39 +01:00
server . log . Debug ( "querying bucket usage report" ,
2019-06-18 00:37:44 +01:00
zap . Stringer ( "projectID" , projectID ) ,
zap . Stringer ( "since" , since ) ,
zap . Stringer ( "before" , before ) )
2019-04-10 00:14:19 +01:00
2020-04-02 13:30:43 +01:00
bucketRollups , err := server . service . GetBucketUsageRollups ( ctx , projectID , since , before )
2019-04-10 00:14:19 +01:00
if err != nil {
2019-09-13 16:38:29 +01:00
server . log . Error ( "bucket usage report error" , zap . Error ( err ) )
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusInternalServerError )
2019-04-10 00:14:19 +01:00
return
}
2019-08-13 13:37:01 +01:00
if err = server . templates . usageReport . Execute ( w , bucketRollups ) ; err != nil {
2019-09-13 16:38:29 +01:00
server . log . Error ( "bucket usage report error" , zap . Error ( err ) )
2019-04-10 00:14:19 +01:00
}
}
2020-01-18 02:34:06 +00:00
// createRegistrationTokenHandler is web app http handler function.
2019-08-08 13:12:39 +01:00
func ( server * Server ) createRegistrationTokenHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( nil )
2019-03-19 17:55:43 +00:00
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 {
2019-08-08 13:12:39 +01:00
server . log . Error ( err . Error ( ) )
2019-03-19 17:55:43 +00:00
}
} ( )
2020-01-14 13:38:32 +00:00
equality := subtle . ConstantTimeCompare (
[ ] byte ( r . Header . Get ( "Authorization" ) ) ,
[ ] byte ( server . config . AuthToken ) ,
)
if equality != 1 {
2019-03-19 17:55:43 +00:00
w . WriteHeader ( 401 )
response . Error = "unauthorized"
return
}
2019-08-08 13:12:39 +01:00
projectsLimitInput := r . URL . Query ( ) . Get ( "projectsLimit" )
2019-03-19 17:55:43 +00:00
projectsLimit , err := strconv . Atoi ( projectsLimitInput )
if err != nil {
response . Error = err . Error ( )
return
}
2019-08-08 13:12:39 +01:00
token , err := server . service . CreateRegToken ( ctx , projectsLimit )
2019-03-19 17:55:43 +00:00
if err != nil {
response . Error = err . Error ( )
return
}
response . Secret = token . Secret . String ( )
}
2020-01-18 02:34:06 +00:00
// populatePromotionalCoupons is web app http handler function for populating promotional coupons.
func ( server * Server ) populatePromotionalCoupons ( w http . ResponseWriter , r * http . Request ) {
2020-01-20 18:57:14 +00:00
var err error
var ctx context . Context
2020-01-18 02:34:06 +00:00
2020-01-20 18:57:14 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2020-01-18 02:34:06 +00:00
2020-01-20 18:57:14 +00:00
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 {
2020-01-18 02:34:06 +00:00
server . log . Error ( "failed to write json error response" , zap . Error ( Error . Wrap ( err ) ) )
}
2020-01-20 18:57:14 +00:00
}
ctx = r . Context ( )
2020-01-18 02:34:06 +00:00
equality := subtle . ConstantTimeCompare (
[ ] byte ( r . Header . Get ( "Authorization" ) ) ,
[ ] byte ( server . config . AuthToken ) ,
)
if equality != 1 {
2020-01-20 18:57:14 +00:00
handleError ( http . StatusUnauthorized , errs . New ( "unauthorized" ) )
2020-01-18 02:34:06 +00:00
return
}
2020-01-20 18:57:14 +00:00
if err = server . service . Payments ( ) . PopulatePromotionalCoupons ( ctx ) ; err != nil {
handleError ( http . StatusInternalServerError , err )
2020-01-18 02:34:06 +00:00
return
}
}
2020-07-16 15:18:02 +01:00
// accountActivationHandler is web app http handler function.
2019-08-08 13:12:39 +01:00
func ( server * Server ) accountActivationHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( nil )
2019-08-08 13:12:39 +01:00
activationToken := r . URL . Query ( ) . Get ( "token" )
2019-03-08 14:01:11 +00:00
2019-08-08 13:12:39 +01:00
err := server . service . ActivateAccount ( ctx , activationToken )
2019-03-08 14:01:11 +00:00
if err != nil {
2019-08-08 13:12:39 +01:00
server . log . Error ( "activation: failed to activate account" ,
2019-04-09 13:20:29 +01:00
zap . String ( "token" , activationToken ) ,
zap . Error ( err ) )
2020-02-10 12:03:38 +00:00
if console . ErrEmailUsed . Has ( err ) {
server . serveError ( w , http . StatusConflict )
return
}
if console . Error . Has ( err ) {
server . serveError ( w , http . StatusInternalServerError )
return
}
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusNotFound )
2019-03-08 14:01:11 +00:00
return
}
2020-03-23 20:38:50 +00:00
http . Redirect ( w , r , server . config . AccountActivationRedirectURL , http . StatusTemporaryRedirect )
2019-03-08 14:01:11 +00:00
}
2019-08-08 13:12:39 +01:00
func ( server * Server ) passwordRecoveryHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( nil )
2019-08-13 13:37:01 +01:00
2019-08-08 13:12:39 +01:00
recoveryToken := r . URL . Query ( ) . Get ( "token" )
2019-04-10 20:16:10 +01:00
if len ( recoveryToken ) == 0 {
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusNotFound )
2019-04-10 20:16:10 +01:00
return
}
2020-01-13 17:30:42 +00:00
var data struct {
SatelliteName string
}
data . SatelliteName = server . config . SatelliteName
2019-08-08 13:12:39 +01:00
switch r . Method {
2019-06-26 14:36:47 +01:00
case http . MethodPost :
2019-08-08 13:12:39 +01:00
err := r . ParseForm ( )
2019-04-10 20:16:10 +01:00
if err != nil {
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusNotFound )
2019-07-25 16:01:44 +01:00
return
2019-04-10 20:16:10 +01:00
}
2019-08-08 13:12:39 +01:00
password := r . FormValue ( "password" )
passwordRepeat := r . FormValue ( "passwordRepeat" )
2019-04-10 20:16:10 +01:00
if strings . Compare ( password , passwordRepeat ) != 0 {
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusNotFound )
2019-04-10 20:16:10 +01:00
return
}
2019-08-08 13:12:39 +01:00
err = server . service . ResetPassword ( ctx , recoveryToken , password )
2019-04-10 20:16:10 +01:00
if err != nil {
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusNotFound )
2019-07-25 16:01:44 +01:00
return
2019-04-10 20:16:10 +01:00
}
2019-07-25 16:01:44 +01:00
2020-01-13 17:30:42 +00:00
if err := server . templates . success . Execute ( w , data ) ; err != nil {
2019-10-31 18:42:28 +00:00
server . log . Error ( "success reset password template could not be executed" , zap . Error ( Error . Wrap ( err ) ) )
2019-07-25 16:01:44 +01:00
return
2019-04-10 20:16:10 +01:00
}
2019-08-13 13:37:01 +01:00
case http . MethodGet :
2020-01-13 17:30:42 +00:00
if err := server . templates . resetPassword . Execute ( w , data ) ; err != nil {
2019-10-31 18:42:28 +00:00
server . log . Error ( "reset password template could not be executed" , zap . Error ( Error . Wrap ( err ) ) )
2019-07-25 16:01:44 +01:00
return
2019-04-10 20:16:10 +01:00
}
2019-06-26 14:36:47 +01:00
default :
2019-10-31 18:42:28 +00:00
server . serveError ( w , http . StatusNotFound )
2019-06-26 14:36:47 +01:00
return
2019-04-10 20:16:10 +01:00
}
}
2019-08-08 13:12:39 +01:00
func ( server * Server ) cancelPasswordRecoveryHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( nil )
2019-08-08 13:12:39 +01:00
recoveryToken := r . URL . Query ( ) . Get ( "token" )
2019-05-13 16:53:52 +01:00
// No need to check error as we anyway redirect user to support page
2019-08-08 13:12:39 +01:00
_ = server . service . RevokeResetPasswordToken ( ctx , recoveryToken )
2019-05-13 16:53:52 +01:00
2019-08-13 13:37:01 +01:00
// TODO: Should place this link to config
2019-08-08 13:12:39 +01:00
http . Redirect ( w , r , "https://storjlabs.atlassian.net/servicedesk/customer/portals" , http . StatusSeeOther )
2019-05-13 16:53:52 +01:00
}
2019-12-12 12:58:15 +00:00
// projectUsageLimitsHandler api handler for project usage limits.
func ( server * Server ) projectUsageLimitsHandler ( w http . ResponseWriter , r * http . Request ) {
err := error ( nil )
ctx := r . Context ( )
2019-08-13 13:37:01 +01:00
2019-12-12 12:58:15 +00:00
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" `
2019-10-31 18:42:28 +00:00
}
2019-12-12 12:58:15 +00:00
2020-12-22 12:05:22 +00:00
// N.B. we are probably leaking internal details to the client
2019-12-12 12:58:15 +00:00
jsonError . Error = err . Error ( )
2020-01-07 10:41:19 +00:00
if err := json . NewEncoder ( w ) . Encode ( jsonError ) ; err != nil {
2019-12-12 12:58:15 +00:00
server . log . Error ( "error encoding project usage limits error" , zap . Error ( err ) )
2019-10-31 18:42:28 +00:00
}
2019-08-13 13:37:01 +01:00
}
2019-12-12 12:58:15 +00:00
handleServiceError := func ( err error ) {
switch {
case console . ErrUnauthorized . Has ( err ) :
handleError ( http . StatusUnauthorized , err )
2020-12-22 12:05:22 +00:00
case accounting . ErrInvalidArgument . Has ( err ) :
handleError ( http . StatusBadRequest , err )
2019-12-12 12:58:15 +00:00
default :
handleError ( http . StatusInternalServerError , err )
}
}
2019-12-19 14:28:25 +00:00
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2019-12-12 12:58:15 +00:00
if idParam , ok = mux . Vars ( r ) [ "id" ] ; ! ok {
handleError ( http . StatusBadRequest , errs . New ( "missing project id route param" ) )
return
}
2020-04-02 13:30:43 +01:00
projectID , err := uuid . FromString ( idParam )
2019-12-12 12:58:15 +00:00
if err != nil {
handleError ( http . StatusBadRequest , errs . New ( "invalid project id: %v" , err ) )
return
}
2020-04-02 13:30:43 +01:00
limits , err := server . service . GetProjectUsageLimits ( ctx , projectID )
2019-12-12 12:58:15 +00:00
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
}
2019-04-10 20:16:10 +01:00
}
2020-10-21 10:58:37 +01:00
// graphqlHandler is graphql endpoint http handler function.
func ( server * Server ) graphqlHandler ( w http . ResponseWriter , r * http . Request ) {
2019-08-08 13:12:39 +01:00
ctx := r . Context ( )
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( nil )
2020-01-20 13:02:44 +00:00
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 ) )
}
}
2019-01-24 16:26:36 +00:00
w . Header ( ) . Set ( contentType , applicationJSON )
2019-09-20 18:40:26 +01:00
query , err := getQuery ( w , r )
2019-01-24 16:26:36 +00:00
if err != nil {
2020-01-20 13:02:44 +00:00
handleError ( http . StatusBadRequest , err )
2019-01-24 16:26:36 +00:00
return
}
2019-03-02 15:22:20 +00:00
rootObject := make ( map [ string ] interface { } )
2019-03-26 15:56:16 +00:00
2019-08-08 13:12:39 +01:00
rootObject [ "origin" ] = server . config . ExternalAddress
2019-03-08 14:01:11 +00:00
rootObject [ consoleql . ActivationPath ] = "activation/?token="
2019-04-10 20:16:10 +01:00
rootObject [ consoleql . PasswordRecoveryPath ] = "password-recovery/?token="
2019-05-13 16:53:52 +01:00
rootObject [ consoleql . CancelPasswordRecoveryPath ] = "cancel-password-recovery/?token="
2019-03-26 15:56:16 +00:00
rootObject [ consoleql . SignInPath ] = "login"
2019-09-27 17:48:53 +01:00
rootObject [ consoleql . LetUsKnowURL ] = server . config . LetUsKnowURL
rootObject [ consoleql . ContactInfoURL ] = server . config . ContactInfoURL
rootObject [ consoleql . TermsAndConditionsURL ] = server . config . TermsAndConditionsURL
2019-03-02 15:22:20 +00:00
2019-01-24 16:26:36 +00:00
result := graphql . Do ( graphql . Params {
2019-08-08 13:12:39 +01:00
Schema : server . schema ,
2019-01-24 16:26:36 +00:00
Context : ctx ,
RequestString : query . Query ,
VariableValues : query . Variables ,
OperationName : query . OperationName ,
2019-03-02 15:22:20 +00:00
RootObject : rootObject ,
2019-01-24 16:26:36 +00:00
} )
2020-01-20 13:02:44 +00:00
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
}
}
2020-02-21 11:47:53 +00:00
handleErrors ( http . StatusOK , result . Errors )
2020-01-20 13:02:44 +00:00
}
if result . HasErrors ( ) {
handleGraphqlErrors ( )
return
}
2019-01-24 16:26:36 +00:00
err = json . NewEncoder ( w ) . Encode ( result )
if err != nil {
2020-01-20 13:02:44 +00:00
server . log . Error ( "error encoding grapql result" , zap . Error ( err ) )
2019-01-24 16:26:36 +00:00
return
}
2020-04-13 10:31:17 +01:00
server . log . Debug ( fmt . Sprintf ( "%s" , result ) )
2019-01-24 16:26:36 +00:00
}
2019-12-12 12:58:15 +00:00
// 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 ) ) )
}
2020-02-10 12:03:38 +00:00
case http . StatusNotFound :
2019-12-12 12:58:15 +00:00
err := server . templates . notFound . Execute ( w , nil )
if err != nil {
server . log . Error ( "cannot parse pageNotFound template" , zap . Error ( Error . Wrap ( err ) ) )
}
2020-02-10 12:03:38 +00:00
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 ) ) )
}
2019-12-12 12:58:15 +00:00
}
}
2020-07-16 15:18:02 +01:00
// seoHandler used to communicate with web crawlers and other web robots.
2019-09-09 19:33:05 +01:00
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" )
2019-09-27 17:48:53 +01:00
_ , err := w . Write ( [ ] byte ( server . config . SEO ) )
2019-09-09 19:33:05 +01:00
if err != nil {
server . log . Error ( err . Error ( ) )
}
}
2020-12-15 19:06:26 +00:00
// brotliMiddleware is used to compress static content using brotli to minify resources if browser support such decoding.
func ( server * Server ) brotliMiddleware ( fn http . Handler ) http . Handler {
2019-08-08 13:12:39 +01:00
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2019-11-12 13:05:35 +00:00
w . Header ( ) . Set ( "Cache-Control" , "public, max-age=31536000" )
w . Header ( ) . Set ( "X-Content-Type-Options" , "nosniff" )
2020-12-15 19:06:26 +00:00
isBrotliSupported := strings . Contains ( r . Header . Get ( "Accept-Encoding" ) , "br" )
if ! isBrotliSupported {
2019-11-12 13:05:35 +00:00
fn . ServeHTTP ( w , r )
return
2019-08-08 13:12:39 +01:00
}
2019-08-13 13:37:01 +01:00
2020-12-15 19:06:26 +00:00
info , err := os . Stat ( server . config . StaticDir + strings . TrimPrefix ( r . URL . Path , "/static" ) + ".br" )
2019-11-12 13:05:35 +00:00
if err != nil {
2019-08-08 13:12:39 +01:00
fn . ServeHTTP ( w , r )
return
}
2019-11-12 13:05:35 +00:00
extension := filepath . Ext ( info . Name ( ) [ : len ( info . Name ( ) ) - 3 ] )
w . Header ( ) . Set ( contentType , mime . TypeByExtension ( extension ) )
2020-12-15 19:06:26 +00:00
w . Header ( ) . Set ( "Content-Encoding" , "br" )
2019-08-08 13:12:39 +01:00
newRequest := new ( http . Request )
* newRequest = * r
newRequest . URL = new ( url . URL )
* newRequest . URL = * r . URL
2020-12-15 19:06:26 +00:00
newRequest . URL . Path += ".br"
2019-08-08 13:12:39 +01:00
fn . ServeHTTP ( w , newRequest )
} )
2019-01-24 16:26:36 +00:00
}
2019-08-13 13:37:01 +01:00
2020-07-16 15:18:02 +01:00
// initializeTemplates is used to initialize all templates.
2019-08-13 13:37:01 +01:00
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 )
}
2019-10-31 18:42:28 +00:00
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" ) )
2019-08-13 13:37:01 +01:00
if err != nil {
return Error . Wrap ( err )
}
return nil
}