51d667a65e
If a visitor to the website (run through the reverse proxy) consented to cookies, read the ID stored in that cookie and send it along with the Identify/Track calls sent to Segment upon account creation. This allows us to connect referral information gathered when visitors land on our website with account activity, helping us improve our onboarding flow. Change-Id: I0ece717ab5bba67901e50a9b4229c1d4ed7e46b7
482 lines
13 KiB
Go
482 lines
13 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package consoleapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/zeebo/errs"
|
|
"go.uber.org/zap"
|
|
|
|
"storj.io/common/uuid"
|
|
"storj.io/storj/private/post"
|
|
"storj.io/storj/satellite/analytics"
|
|
"storj.io/storj/satellite/console"
|
|
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
|
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
|
|
"storj.io/storj/satellite/mailservice"
|
|
"storj.io/storj/satellite/rewards"
|
|
)
|
|
|
|
var (
|
|
// ErrAuthAPI - console auth api error type.
|
|
ErrAuthAPI = errs.Class("console auth api error")
|
|
|
|
// errNotImplemented is the error value used by handlers of this package to
|
|
// response with status Not Implemented.
|
|
errNotImplemented = errs.New("not implemented")
|
|
)
|
|
|
|
// Auth is an api controller that exposes all auth functionality.
|
|
type Auth struct {
|
|
log *zap.Logger
|
|
ExternalAddress string
|
|
LetUsKnowURL string
|
|
TermsAndConditionsURL string
|
|
ContactInfoURL string
|
|
service *console.Service
|
|
analytics *analytics.Service
|
|
mailService *mailservice.Service
|
|
cookieAuth *consolewebauth.CookieAuth
|
|
partners *rewards.PartnersService
|
|
}
|
|
|
|
// NewAuth is a constructor for api auth controller.
|
|
func NewAuth(log *zap.Logger, service *console.Service, mailService *mailservice.Service, cookieAuth *consolewebauth.CookieAuth, partners *rewards.PartnersService, analytics *analytics.Service, externalAddress string, letUsKnowURL string, termsAndConditionsURL string, contactInfoURL string) *Auth {
|
|
return &Auth{
|
|
log: log,
|
|
ExternalAddress: externalAddress,
|
|
LetUsKnowURL: letUsKnowURL,
|
|
TermsAndConditionsURL: termsAndConditionsURL,
|
|
ContactInfoURL: contactInfoURL,
|
|
service: service,
|
|
mailService: mailService,
|
|
cookieAuth: cookieAuth,
|
|
partners: partners,
|
|
analytics: analytics,
|
|
}
|
|
}
|
|
|
|
// Token authenticates user by credentials and returns auth token.
|
|
func (a *Auth) Token(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
var err error
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
var tokenRequest struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
err = json.NewDecoder(r.Body).Decode(&tokenRequest)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
token, err := a.service.Token(ctx, tokenRequest.Email, tokenRequest.Password)
|
|
if err != nil {
|
|
a.log.Info("Error authenticating token request", zap.String("email", tokenRequest.Email), zap.Error(ErrAuthAPI.Wrap(err)))
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
a.cookieAuth.SetTokenCookie(w, token)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
err = json.NewEncoder(w).Encode(token)
|
|
if err != nil {
|
|
a.log.Error("token handler could not encode token response", zap.Error(ErrAuthAPI.Wrap(err)))
|
|
return
|
|
}
|
|
}
|
|
|
|
// Logout removes auth cookie.
|
|
func (a *Auth) Logout(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
defer mon.Task()(&ctx)(nil)
|
|
|
|
a.cookieAuth.RemoveTokenCookie(w)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
}
|
|
|
|
// Register creates new user, sends activation e-mail.
|
|
func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
var err error
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
var registerData struct {
|
|
FullName string `json:"fullName"`
|
|
ShortName string `json:"shortName"`
|
|
Email string `json:"email"`
|
|
Partner string `json:"partner"`
|
|
PartnerID string `json:"partnerId"`
|
|
Password string `json:"password"`
|
|
SecretInput string `json:"secret"`
|
|
ReferrerUserID string `json:"referrerUserId"`
|
|
IsProfessional bool `json:"isProfessional"`
|
|
Position string `json:"position"`
|
|
CompanyName string `json:"companyName"`
|
|
EmployeeCount string `json:"employeeCount"`
|
|
}
|
|
|
|
err = json.NewDecoder(r.Body).Decode(®isterData)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
secret, err := console.RegistrationSecretFromBase64(registerData.SecretInput)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
if registerData.Partner != "" {
|
|
info, err := a.partners.ByName(ctx, registerData.Partner)
|
|
if err != nil {
|
|
a.log.Warn("Invalid partner name", zap.String("Partner name", registerData.Partner), zap.String("User email", registerData.Email), zap.Error(err))
|
|
} else {
|
|
registerData.PartnerID = info.ID
|
|
}
|
|
}
|
|
|
|
user, err := a.service.CreateUser(ctx,
|
|
console.CreateUser{
|
|
FullName: registerData.FullName,
|
|
ShortName: registerData.ShortName,
|
|
Email: registerData.Email,
|
|
PartnerID: registerData.PartnerID,
|
|
Password: registerData.Password,
|
|
IsProfessional: registerData.IsProfessional,
|
|
Position: registerData.Position,
|
|
CompanyName: registerData.CompanyName,
|
|
EmployeeCount: registerData.EmployeeCount,
|
|
},
|
|
secret,
|
|
)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
trackCreateUserFields := analytics.TrackCreateUserFields{
|
|
ID: user.ID,
|
|
AnonymousID: loadSession(r),
|
|
FullName: user.FullName,
|
|
Email: user.Email,
|
|
Type: analytics.Personal,
|
|
}
|
|
if user.IsProfessional {
|
|
trackCreateUserFields.Type = analytics.Professional
|
|
trackCreateUserFields.EmployeeCount = user.EmployeeCount
|
|
trackCreateUserFields.CompanyName = user.CompanyName
|
|
trackCreateUserFields.JobTitle = user.Position
|
|
}
|
|
a.analytics.TrackCreateUser(trackCreateUserFields)
|
|
|
|
token, err := a.service.GenerateActivationToken(ctx, user.ID, user.Email)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
link := a.ExternalAddress + "activation/?token=" + token
|
|
userName := user.ShortName
|
|
if user.ShortName == "" {
|
|
userName = user.FullName
|
|
}
|
|
|
|
a.mailService.SendRenderedAsync(
|
|
ctx,
|
|
[]post.Address{{Address: user.Email, Name: userName}},
|
|
&consoleql.AccountActivationEmail{
|
|
ActivationLink: link,
|
|
Origin: a.ExternalAddress,
|
|
UserName: userName,
|
|
},
|
|
)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
err = json.NewEncoder(w).Encode(user.ID)
|
|
if err != nil {
|
|
a.log.Error("registration handler could not encode userID", zap.Error(ErrAuthAPI.Wrap(err)))
|
|
return
|
|
}
|
|
}
|
|
|
|
// loadSession looks for a cookie for the session id.
|
|
// this cookie is set from the reverse proxy if the user opts into cookies from Storj.
|
|
func loadSession(req *http.Request) string {
|
|
sessionCookie, err := req.Cookie("webtraf-sid")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return sessionCookie.Value
|
|
}
|
|
|
|
// UpdateAccount updates user's full name and short name.
|
|
func (a *Auth) UpdateAccount(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
var err error
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
var updatedInfo struct {
|
|
FullName string `json:"fullName"`
|
|
ShortName string `json:"shortName"`
|
|
}
|
|
|
|
err = json.NewDecoder(r.Body).Decode(&updatedInfo)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
if err = a.service.UpdateAccount(ctx, updatedInfo.FullName, updatedInfo.ShortName); err != nil {
|
|
a.serveJSONError(w, err)
|
|
}
|
|
}
|
|
|
|
// GetAccount gets authorized user and take it's params.
|
|
func (a *Auth) GetAccount(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
var err error
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
var user struct {
|
|
ID uuid.UUID `json:"id"`
|
|
FullName string `json:"fullName"`
|
|
ShortName string `json:"shortName"`
|
|
Email string `json:"email"`
|
|
PartnerID uuid.UUID `json:"partnerId"`
|
|
ProjectLimit int `json:"projectLimit"`
|
|
IsProfessional bool `json:"isProfessional"`
|
|
Position string `json:"position"`
|
|
CompanyName string `json:"companyName"`
|
|
EmployeeCount string `json:"employeeCount"`
|
|
}
|
|
|
|
auth, err := console.GetAuth(ctx)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
user.ShortName = auth.User.ShortName
|
|
user.FullName = auth.User.FullName
|
|
user.Email = auth.User.Email
|
|
user.ID = auth.User.ID
|
|
user.PartnerID = auth.User.PartnerID
|
|
user.ProjectLimit = auth.User.ProjectLimit
|
|
user.IsProfessional = auth.User.IsProfessional
|
|
user.CompanyName = auth.User.CompanyName
|
|
user.Position = auth.User.Position
|
|
user.EmployeeCount = auth.User.EmployeeCount
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
err = json.NewEncoder(w).Encode(&user)
|
|
if err != nil {
|
|
a.log.Error("could not encode user info", zap.Error(ErrAuthAPI.Wrap(err)))
|
|
return
|
|
}
|
|
}
|
|
|
|
// DeleteAccount authorizes user and deletes account by password.
|
|
func (a *Auth) DeleteAccount(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
defer mon.Task()(&ctx)(&errNotImplemented)
|
|
|
|
// We do not want to allow account deletion via API currently.
|
|
a.serveJSONError(w, errNotImplemented)
|
|
}
|
|
|
|
// ChangeEmail auth user, changes users email for a new one.
|
|
func (a *Auth) ChangeEmail(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
var err error
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
var emailChange struct {
|
|
NewEmail string `json:"newEmail"`
|
|
}
|
|
|
|
err = json.NewDecoder(r.Body).Decode(&emailChange)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
err = a.service.ChangeEmail(ctx, emailChange.NewEmail)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// ChangePassword auth user, changes users password for a new one.
|
|
func (a *Auth) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
var err error
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
var passwordChange struct {
|
|
CurrentPassword string `json:"password"`
|
|
NewPassword string `json:"newPassword"`
|
|
}
|
|
|
|
err = json.NewDecoder(r.Body).Decode(&passwordChange)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
err = a.service.ChangePassword(ctx, passwordChange.CurrentPassword, passwordChange.NewPassword)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// ForgotPassword creates password-reset token and sends email to user.
|
|
func (a *Auth) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
var err error
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
params := mux.Vars(r)
|
|
email, ok := params["email"]
|
|
if !ok {
|
|
err = errs.New("email expected")
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
user, err := a.service.GetUserByEmail(ctx, email)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
recoveryToken, err := a.service.GeneratePasswordRecoveryToken(ctx, user.ID)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
passwordRecoveryLink := a.ExternalAddress + "password-recovery/?token=" + recoveryToken
|
|
cancelPasswordRecoveryLink := a.ExternalAddress + "cancel-password-recovery/?token=" + recoveryToken
|
|
userName := user.ShortName
|
|
if user.ShortName == "" {
|
|
userName = user.FullName
|
|
}
|
|
|
|
contactInfoURL := a.ContactInfoURL
|
|
letUsKnowURL := a.LetUsKnowURL
|
|
termsAndConditionsURL := a.TermsAndConditionsURL
|
|
|
|
a.mailService.SendRenderedAsync(
|
|
ctx,
|
|
[]post.Address{{Address: user.Email, Name: userName}},
|
|
&consoleql.ForgotPasswordEmail{
|
|
Origin: a.ExternalAddress,
|
|
UserName: userName,
|
|
ResetLink: passwordRecoveryLink,
|
|
CancelPasswordRecoveryLink: cancelPasswordRecoveryLink,
|
|
LetUsKnowURL: letUsKnowURL,
|
|
ContactInfoURL: contactInfoURL,
|
|
TermsAndConditionsURL: termsAndConditionsURL,
|
|
},
|
|
)
|
|
}
|
|
|
|
// ResendEmail generates activation token by userID and sends email account activation email to user.
|
|
func (a *Auth) ResendEmail(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
var err error
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
params := mux.Vars(r)
|
|
id, ok := params["id"]
|
|
if !ok {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
userID, err := uuid.FromString(id)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
user, err := a.service.GetUser(ctx, userID)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
token, err := a.service.GenerateActivationToken(ctx, user.ID, user.Email)
|
|
if err != nil {
|
|
a.serveJSONError(w, err)
|
|
return
|
|
}
|
|
|
|
link := a.ExternalAddress + "activation/?token=" + token
|
|
userName := user.ShortName
|
|
if user.ShortName == "" {
|
|
userName = user.FullName
|
|
}
|
|
|
|
contactInfoURL := a.ContactInfoURL
|
|
termsAndConditionsURL := a.TermsAndConditionsURL
|
|
|
|
a.mailService.SendRenderedAsync(
|
|
ctx,
|
|
[]post.Address{{Address: user.Email, Name: userName}},
|
|
&consoleql.AccountActivationEmail{
|
|
Origin: a.ExternalAddress,
|
|
ActivationLink: link,
|
|
TermsAndConditionsURL: termsAndConditionsURL,
|
|
ContactInfoURL: contactInfoURL,
|
|
UserName: userName,
|
|
},
|
|
)
|
|
}
|
|
|
|
// serveJSONError writes JSON error to response output stream.
|
|
func (a *Auth) serveJSONError(w http.ResponseWriter, err error) {
|
|
w.WriteHeader(a.getStatusCode(err))
|
|
|
|
var response struct {
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
response.Error = err.Error()
|
|
|
|
err = json.NewEncoder(w).Encode(response)
|
|
if err != nil {
|
|
a.log.Error("failed to write json error response", zap.Error(ErrAuthAPI.Wrap(err)))
|
|
}
|
|
}
|
|
|
|
// getStatusCode returns http.StatusCode depends on console error class.
|
|
func (a *Auth) getStatusCode(err error) int {
|
|
switch {
|
|
case console.ErrValidation.Has(err):
|
|
return http.StatusBadRequest
|
|
case console.ErrUnauthorized.Has(err):
|
|
return http.StatusUnauthorized
|
|
case console.ErrEmailUsed.Has(err):
|
|
return http.StatusConflict
|
|
case errors.Is(err, errNotImplemented):
|
|
return http.StatusNotImplemented
|
|
default:
|
|
return http.StatusInternalServerError
|
|
}
|
|
}
|