aa691dbc09
We were using verify_token="" to mean that the user was verified. We need a unique constraint on verify_token to prevent two users from getting the same verify link in their email. This means that if we have two verified users, they will both have verify_token="", which triggers the unique constraint. Oops. However, null is an exception to unique constraints, so we're now using that instead to mean verified.
231 lines
6.1 KiB
Go
231 lines
6.1 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
|
|
"lbryio/lbry-id/auth"
|
|
"lbryio/lbry-id/env"
|
|
"lbryio/lbry-id/store"
|
|
)
|
|
|
|
type RegisterRequest struct {
|
|
Email auth.Email `json:"email"`
|
|
Password auth.Password `json:"password"`
|
|
ClientSaltSeed auth.ClientSaltSeed `json:"clientSaltSeed"`
|
|
}
|
|
|
|
type RegisterResponse struct {
|
|
Verified bool `json:"verified"`
|
|
}
|
|
|
|
func (r *RegisterRequest) validate() error {
|
|
if !r.Email.Validate() {
|
|
return fmt.Errorf("Invalid or missing 'email'")
|
|
}
|
|
if r.Password == "" {
|
|
return fmt.Errorf("Missing 'password'")
|
|
}
|
|
|
|
if !r.ClientSaltSeed.Validate() {
|
|
return fmt.Errorf("Invalid or missing 'clientSaltSeed'")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) register(w http.ResponseWriter, req *http.Request) {
|
|
var registerRequest RegisterRequest
|
|
if !getPostData(w, req, ®isterRequest) {
|
|
return
|
|
}
|
|
|
|
verificationMode, err := env.GetAccountVerificationMode(s.env)
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error getting account verification mode")
|
|
return
|
|
}
|
|
accountWhitelist, err := env.GetAccountWhitelist(s.env, verificationMode)
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error getting account whitelist")
|
|
return
|
|
}
|
|
|
|
var registerResponse RegisterResponse
|
|
|
|
var token *auth.VerifyTokenString
|
|
|
|
modes:
|
|
switch verificationMode {
|
|
case env.AccountVerificationModeAllowAll:
|
|
// Always verified (for testers). No need to jump through email verify
|
|
// hoops.
|
|
registerResponse.Verified = true
|
|
case env.AccountVerificationModeWhitelist:
|
|
for _, whitelisteEmail := range accountWhitelist {
|
|
if whitelisteEmail == registerRequest.Email {
|
|
registerResponse.Verified = true
|
|
break modes
|
|
}
|
|
}
|
|
// If we have unverified users on whitelist setups, we'd need to create a way
|
|
// to verify them. It's easier to just prevent account creation. It also will
|
|
// make it easier for self-hosters to figure out that something is wrong
|
|
// with their whitelist.
|
|
errorJson(w, http.StatusForbidden, "Account not whitelisted")
|
|
return
|
|
case env.AccountVerificationModeEmailVerify:
|
|
// Not verified until they click their email link.
|
|
registerResponse.Verified = false
|
|
newToken, err := s.auth.NewVerifyTokenString()
|
|
token = &newToken
|
|
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error generating verify token string")
|
|
return
|
|
}
|
|
}
|
|
|
|
err = s.store.CreateAccount(
|
|
registerRequest.Email,
|
|
registerRequest.Password,
|
|
registerRequest.ClientSaltSeed,
|
|
token, // if it's not set, the user is marked as verified
|
|
)
|
|
|
|
if err != nil {
|
|
if err == store.ErrDuplicateEmail || err == store.ErrDuplicateAccount {
|
|
errorJson(w, http.StatusConflict, "Error registering")
|
|
} else {
|
|
internalServiceErrorJson(w, err, "Error registering")
|
|
}
|
|
return
|
|
}
|
|
|
|
if token != nil {
|
|
err = s.mail.SendVerificationEmail(registerRequest.Email, *token)
|
|
}
|
|
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error sending verification email")
|
|
return
|
|
}
|
|
|
|
response, err := json.Marshal(registerResponse)
|
|
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error generating register response")
|
|
return
|
|
}
|
|
|
|
// TODO StatusCreated also for first wallet and/or for get auth token?
|
|
w.WriteHeader(http.StatusCreated)
|
|
fmt.Fprintf(w, string(response))
|
|
}
|
|
|
|
// TODO - There's probably a struct-based solution here like with POST/PUT.
|
|
// We could put that struct up top as well.
|
|
func getVerifyParams(req *http.Request) (token auth.VerifyTokenString, err error) {
|
|
tokenSlice, hasTokenSlice := req.URL.Query()["verifyToken"]
|
|
|
|
if !hasTokenSlice || tokenSlice[0] == "" {
|
|
err = fmt.Errorf("Missing verifyToken parameter")
|
|
}
|
|
|
|
if err == nil {
|
|
token = auth.VerifyTokenString(tokenSlice[0])
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type ResendVerifyEmailRequest struct {
|
|
Email auth.Email `json:"email"`
|
|
}
|
|
|
|
func (r *ResendVerifyEmailRequest) validate() error {
|
|
if !r.Email.Validate() {
|
|
return fmt.Errorf("Invalid or missing 'email'")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) resendVerifyEmail(w http.ResponseWriter, req *http.Request) {
|
|
verificationMode, err := env.GetAccountVerificationMode(s.env)
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error getting account verification mode")
|
|
return
|
|
}
|
|
if verificationMode != env.AccountVerificationModeEmailVerify {
|
|
errorJson(w, http.StatusForbidden, "Account verification mode is not set to EmailVerify")
|
|
return
|
|
}
|
|
|
|
var resendVerifyEmailRequest ResendVerifyEmailRequest
|
|
if !getPostData(w, req, &resendVerifyEmailRequest) {
|
|
return
|
|
}
|
|
|
|
token, err := s.auth.NewVerifyTokenString()
|
|
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error generating verify token string")
|
|
return
|
|
}
|
|
|
|
err = s.store.UpdateVerifyTokenString(resendVerifyEmailRequest.Email, token)
|
|
if err == store.ErrWrongCredentials {
|
|
errorJson(w, http.StatusUnauthorized, "No match for email")
|
|
return
|
|
}
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error updating verify token string")
|
|
return
|
|
}
|
|
|
|
err = s.mail.SendVerificationEmail(resendVerifyEmailRequest.Email, token)
|
|
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error re-sending verification email")
|
|
return
|
|
}
|
|
|
|
var verifyResponse struct{}
|
|
response, err := json.Marshal(verifyResponse)
|
|
|
|
if err != nil {
|
|
internalServiceErrorJson(w, err, "Error generating verify response")
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(w, string(response))
|
|
}
|
|
|
|
func (s *Server) verify(w http.ResponseWriter, req *http.Request) {
|
|
if !getGetData(w, req) {
|
|
return
|
|
}
|
|
|
|
token, paramsErr := getVerifyParams(req)
|
|
|
|
if paramsErr != nil {
|
|
// In this specific case, the error is limited to values that are safe to
|
|
// give to the user.
|
|
http.Error(w, "There seems to be a problem with this URL: "+paramsErr.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
err := s.store.VerifyAccount(token)
|
|
|
|
if err == store.ErrNoTokenForUser {
|
|
http.Error(w, "The verification token was not found, already used, or expired. If you want to try again, generate a new one from your app.", http.StatusForbidden)
|
|
return
|
|
} else if err != nil {
|
|
http.Error(w, "Something went wrong trying to verify your account.", http.StatusInternalServerError)
|
|
log.Printf("%s: %+v\n", "Error verifying account", err)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(w, "Your account has been verified.")
|
|
}
|