auth
auth/profile.go
Support email verification. The bulk of this commit is auto-modifying files to export variables (mostly our request error types and our response type) so that they can be reused in a Go client for that API. We also implement the beginnings of a Go client for that API, implementing the bare minimum we need for our immediate purposes: the ability to retrieve information about a Login. This, of course, means we need an API endpoint that will return information about a Login, which in turn required us to implement a GetLogin method in our profileStore. Which got in-memory and postgres implementations. That done, we could add the Verification field and Verified field to the Login type, to keep track of whether we've verified the user's ownership of those communication methods (if the Login is, in fact, a communication method). This required us to update sql/postgres_init.sql to account for the new fields we're tracking. It also means that when creating a Login, we had to generate a UUID to use as the Verification field. To make things complete, we needed a verifyLogin method on the profileStore to mark a Login as verified. That, in turn, required an endpoint to control this through the API. While doing so, I lumped things together in an UpdateLogin handler just so we could reuse the endpoint and logic when resending a verification email that may have never reached the user, for whatever reason (the quintessential "send again" button). Finally, we implemented an email_verification listener that will pull email_verification events off NSQ, check for the requisite data integrity, and use mailgun to email out a verification/welcome email.
1.1 --- a/profile.go Sun May 17 02:18:07 2015 -0400 1.2 +++ b/profile.go Sun May 17 02:27:36 2015 -0400 1.3 @@ -39,6 +39,8 @@ 1.4 ErrLoginAlreadyExists = errors.New("login already exists in profileStore") 1.5 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore. 1.6 ErrLoginNotFound = errors.New("login not found in profileStore") 1.7 + // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code. 1.8 + ErrLoginVerificationInvalid = errors.New("login verification code incorrect") 1.9 1.10 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a 1.11 // Passphrase, and requires one. 1.12 @@ -194,11 +196,18 @@ 1.13 // a given Profile that can be used to log into that Profile. 1.14 // Each Profile may only have one Login for each Type. 1.15 type Login struct { 1.16 - Type string `json:"type,omitempty"` 1.17 - Value string `json:"value,omitempty"` 1.18 - ProfileID uuid.ID `json:"profile_id,omitempty"` 1.19 - Created time.Time `json:"created,omitempty"` 1.20 - LastUsed time.Time `json:"last_used,omitempty"` 1.21 + Type string `json:"type,omitempty"` 1.22 + Value string `json:"value,omitempty"` 1.23 + ProfileID uuid.ID `json:"profile_id,omitempty"` 1.24 + Created time.Time `json:"created,omitempty"` 1.25 + LastUsed time.Time `json:"last_used,omitempty"` 1.26 + Verification string `json:"-"` 1.27 + Verified bool `json:"verified"` 1.28 +} 1.29 + 1.30 +type LoginChange struct { 1.31 + Verification *string `json:"verification,omitempty"` 1.32 + ResendVerification *bool `json:"resend_verification,omitempty"` 1.33 } 1.34 1.35 type newProfileRequest struct { 1.36 @@ -207,44 +216,44 @@ 1.37 Name string `json:"name"` 1.38 } 1.39 1.40 -func validateNewProfileRequest(req *newProfileRequest) []requestError { 1.41 - errors := []requestError{} 1.42 +func validateNewProfileRequest(req *newProfileRequest) []RequestError { 1.43 + errors := []RequestError{} 1.44 req.Name = strings.TrimSpace(req.Name) 1.45 req.Email = strings.TrimSpace(req.Email) 1.46 if len(req.Passphrase) < MinPassphraseLength { 1.47 - errors = append(errors, requestError{ 1.48 - Slug: requestErrInsufficient, 1.49 + errors = append(errors, RequestError{ 1.50 + Slug: RequestErrInsufficient, 1.51 Field: "/passphrase", 1.52 }) 1.53 } 1.54 if len(req.Passphrase) > MaxPassphraseLength { 1.55 - errors = append(errors, requestError{ 1.56 - Slug: requestErrOverflow, 1.57 + errors = append(errors, RequestError{ 1.58 + Slug: RequestErrOverflow, 1.59 Field: "/passphrase", 1.60 }) 1.61 } 1.62 if len(req.Name) > MaxNameLength { 1.63 - errors = append(errors, requestError{ 1.64 - Slug: requestErrOverflow, 1.65 + errors = append(errors, RequestError{ 1.66 + Slug: RequestErrOverflow, 1.67 Field: "/name", 1.68 }) 1.69 } 1.70 if req.Email == "" { 1.71 - errors = append(errors, requestError{ 1.72 - Slug: requestErrMissing, 1.73 + errors = append(errors, RequestError{ 1.74 + Slug: RequestErrMissing, 1.75 Field: "/email", 1.76 }) 1.77 } 1.78 if len(req.Email) > MaxEmailLength { 1.79 - errors = append(errors, requestError{ 1.80 - Slug: requestErrOverflow, 1.81 + errors = append(errors, RequestError{ 1.82 + Slug: RequestErrOverflow, 1.83 Field: "/email", 1.84 }) 1.85 } 1.86 re := regexp.MustCompile(".+@.+\\..+") 1.87 if !re.Match([]byte(req.Email)) { 1.88 - errors = append(errors, requestError{ 1.89 - Slug: requestErrInvalidFormat, 1.90 + errors = append(errors, RequestError{ 1.91 + Slug: RequestErrInvalidFormat, 1.92 Field: "/email", 1.93 }) 1.94 } 1.95 @@ -260,9 +269,11 @@ 1.96 deleteProfile(id uuid.ID) error 1.97 1.98 addLogin(login Login) error 1.99 + getLogin(value string) (Login, error) 1.100 removeLogin(value string, profile uuid.ID) error 1.101 removeLoginsByProfile(profile uuid.ID) error 1.102 recordLoginUse(value string, when time.Time) error 1.103 + verifyLogin(value, verification string) error 1.104 listLogins(profile uuid.ID, num, offset int) ([]Login, error) 1.105 } 1.106 1.107 @@ -352,6 +363,16 @@ 1.108 return nil 1.109 } 1.110 1.111 +func (m *memstore) getLogin(value string) (Login, error) { 1.112 + m.loginLock.RLock() 1.113 + defer m.loginLock.RUnlock() 1.114 + l, ok := m.logins[value] 1.115 + if !ok { 1.116 + return Login{}, ErrLoginNotFound 1.117 + } 1.118 + return l, nil 1.119 +} 1.120 + 1.121 func (m *memstore) removeLogin(value string, profile uuid.ID) error { 1.122 m.loginLock.Lock() 1.123 defer m.loginLock.Unlock() 1.124 @@ -402,6 +423,21 @@ 1.125 return nil 1.126 } 1.127 1.128 +func (m *memstore) verifyLogin(value, verification string) error { 1.129 + m.loginLock.Lock() 1.130 + defer m.loginLock.Unlock() 1.131 + l, ok := m.logins[value] 1.132 + if !ok { 1.133 + return ErrLoginNotFound 1.134 + } 1.135 + if l.Verification != verification { 1.136 + return ErrLoginVerificationInvalid 1.137 + } 1.138 + l.Verified = true 1.139 + m.logins[value] = l 1.140 + return nil 1.141 +} 1.142 + 1.143 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) { 1.144 m.loginLock.RLock() 1.145 defer m.loginLock.RUnlock() 1.146 @@ -466,35 +502,37 @@ 1.147 // BUG(paddy): We need to implement a handler that will add a login to a profile. 1.148 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login? 1.149 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile. 1.150 + r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS") 1.151 + r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS") 1.152 } 1.153 1.154 // GetProfileHandler is an HTTP handler for retrieving a profile. 1.155 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.156 - errors := []requestError{} 1.157 + errors := []RequestError{} 1.158 authz := r.Header.Get("Authorization") 1.159 if !strings.HasPrefix(authz, "Bearer ") { 1.160 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.161 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 1.162 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.163 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 1.164 return 1.165 } 1.166 authz = strings.TrimPrefix(authz, "Bearer ") 1.167 vars := mux.Vars(r) 1.168 if vars["id"] == "" { 1.169 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 1.170 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.171 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 1.172 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.173 return 1.174 } 1.175 id, err := uuid.Parse(vars["id"]) 1.176 if err != nil { 1.177 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) 1.178 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.179 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) 1.180 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.181 return 1.182 } 1.183 token, err := context.GetToken(authz, false) 1.184 if err != nil || token.Revoked { 1.185 if err == ErrTokenNotFound || token.Revoked { 1.186 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.187 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 1.188 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.189 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 1.190 return 1.191 } else { 1.192 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 1.193 @@ -502,21 +540,21 @@ 1.194 } 1.195 } 1.196 if !id.Equal(token.ProfileID) { 1.197 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.198 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 1.199 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.200 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 1.201 return 1.202 } 1.203 profile, err := context.GetProfileByID(id) 1.204 if err != nil { 1.205 if err == ErrProfileNotFound { 1.206 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 1.207 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 1.208 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) 1.209 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 1.210 return 1.211 } 1.212 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 1.213 return 1.214 } 1.215 - encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}}) 1.216 + encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) 1.217 return 1.218 } 1.219 1.220 @@ -529,7 +567,7 @@ 1.221 return 1.222 } 1.223 var req newProfileRequest 1.224 - errors := []requestError{} 1.225 + errors := []RequestError{} 1.226 decoder := json.NewDecoder(r.Body) 1.227 err := decoder.Decode(&req) 1.228 if err != nil { 1.229 @@ -538,7 +576,7 @@ 1.230 } 1.231 errors = append(errors, validateNewProfileRequest(&req)...) 1.232 if len(errors) > 0 { 1.233 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.234 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.235 return 1.236 } 1.237 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations) 1.238 @@ -560,7 +598,7 @@ 1.239 err = context.SaveProfile(profile) 1.240 if err != nil { 1.241 if err == ErrProfileAlreadyExists { 1.242 - encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}}) 1.243 + encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}}) 1.244 return 1.245 } 1.246 log.Printf("Error saving profile: %#+v\n", err) 1.247 @@ -569,16 +607,17 @@ 1.248 } 1.249 logins := []Login{} 1.250 login := Login{ 1.251 - Type: "email", 1.252 - Value: req.Email, 1.253 - Created: profile.Created, 1.254 - LastUsed: profile.Created, 1.255 - ProfileID: profile.ID, 1.256 + Type: "email", 1.257 + Value: req.Email, 1.258 + Created: profile.Created, 1.259 + LastUsed: profile.Created, 1.260 + ProfileID: profile.ID, 1.261 + Verification: uuid.NewID().String(), 1.262 } 1.263 err = context.AddLogin(login) 1.264 if err != nil { 1.265 if err == ErrLoginAlreadyExists { 1.266 - encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}}) 1.267 + encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}}) 1.268 return 1.269 } 1.270 log.Printf("Error adding login: %#+v\n", err) 1.271 @@ -586,48 +625,48 @@ 1.272 return 1.273 } 1.274 logins = append(logins, login) 1.275 - resp := response{ 1.276 + resp := Response{ 1.277 Logins: logins, 1.278 Profiles: []Profile{profile}, 1.279 } 1.280 encode(w, r, http.StatusCreated, resp) 1.281 - // TODO(paddy): should we kick off the email validation flow? 1.282 + go context.SendLoginVerification(login) 1.283 } 1.284 1.285 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.286 - errors := []requestError{} 1.287 + errors := []RequestError{} 1.288 vars := mux.Vars(r) 1.289 if vars["id"] == "" { 1.290 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 1.291 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.292 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 1.293 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.294 return 1.295 } 1.296 id, err := uuid.Parse(vars["id"]) 1.297 if err != nil { 1.298 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.299 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.300 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.301 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.302 return 1.303 } 1.304 username, password, ok := r.BasicAuth() 1.305 if !ok { 1.306 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.307 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 1.308 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.309 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 1.310 return 1.311 } 1.312 profile, err := authenticate(username, password, context) 1.313 if err != nil { 1.314 if isAuthError(err) { 1.315 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.316 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 1.317 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.318 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 1.319 } else { 1.320 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 1.321 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 1.322 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 1.323 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 1.324 } 1.325 return 1.326 } 1.327 if !profile.ID.Equal(id) { 1.328 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.329 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 1.330 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.331 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 1.332 return 1.333 } 1.334 var req ProfileChange 1.335 @@ -646,13 +685,13 @@ 1.336 req.LastSeen = nil 1.337 if req.Passphrase != nil { 1.338 if len(*req.Passphrase) < MinPassphraseLength { 1.339 - errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/passphrase"}) 1.340 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.341 + errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"}) 1.342 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.343 return 1.344 } 1.345 if len(*req.Passphrase) > MaxPassphraseLength { 1.346 - errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/passphrase"}) 1.347 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.348 + errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"}) 1.349 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.350 return 1.351 } 1.352 iterations := context.config.iterations 1.353 @@ -679,13 +718,13 @@ 1.354 err = req.Validate() 1.355 if err != nil { 1.356 var status int 1.357 - var resp response 1.358 + var resp Response 1.359 switch err { 1.360 case ErrEmptyChange: 1.361 resp.Profiles = []Profile{profile} 1.362 status = http.StatusOK 1.363 default: 1.364 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 1.365 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 1.366 resp.Errors = errors 1.367 status = http.StatusInternalServerError 1.368 } 1.369 @@ -695,64 +734,145 @@ 1.370 err = context.UpdateProfile(id, req) 1.371 if err != nil { 1.372 if err == ErrProfileNotFound { 1.373 - errors = append(errors, requestError{Slug: requestErrNotFound}) 1.374 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 1.375 + errors = append(errors, RequestError{Slug: RequestErrNotFound}) 1.376 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 1.377 return 1.378 } 1.379 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 1.380 return 1.381 } 1.382 profile.ApplyChange(req) 1.383 - encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}}) 1.384 + encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) 1.385 return 1.386 } 1.387 1.388 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.389 - errors := []requestError{} 1.390 + errors := []RequestError{} 1.391 vars := mux.Vars(r) 1.392 if vars["id"] == "" { 1.393 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 1.394 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.395 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 1.396 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.397 return 1.398 } 1.399 id, err := uuid.Parse(vars["id"]) 1.400 if err != nil { 1.401 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.402 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.403 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.404 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.405 return 1.406 } 1.407 username, password, ok := r.BasicAuth() 1.408 if !ok { 1.409 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.410 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 1.411 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.412 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 1.413 return 1.414 } 1.415 profile, err := authenticate(username, password, context) 1.416 if err != nil { 1.417 if isAuthError(err) { 1.418 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.419 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 1.420 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.421 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 1.422 } else { 1.423 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 1.424 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 1.425 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 1.426 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 1.427 } 1.428 return 1.429 } 1.430 if !profile.ID.Equal(id) { 1.431 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 1.432 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 1.433 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 1.434 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 1.435 return 1.436 } 1.437 err = context.DeleteProfile(id) 1.438 if err != nil { 1.439 if err == ErrProfileNotFound { 1.440 - errors = append(errors, requestError{Slug: requestErrNotFound}) 1.441 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 1.442 + errors = append(errors, RequestError{Slug: RequestErrNotFound}) 1.443 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 1.444 return 1.445 } 1.446 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 1.447 return 1.448 } 1.449 - encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}}) 1.450 + encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) 1.451 go cleanUpAfterProfileDeletion(profile.ID, context) 1.452 } 1.453 + 1.454 +func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.455 + var errors []RequestError 1.456 + vars := mux.Vars(r) 1.457 + if vars["login"] == "" { 1.458 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"}) 1.459 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.460 + return 1.461 + } 1.462 + login, err := context.GetLogin(vars["login"]) 1.463 + if err != nil { 1.464 + if err == ErrLoginNotFound { 1.465 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) 1.466 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 1.467 + return 1.468 + } 1.469 + log.Printf("Error retrieving login: %#+v\n", err) 1.470 + encode(w, r, http.StatusInternalServerError, actOfGodResponse) 1.471 + return 1.472 + } 1.473 + encode(w, r, http.StatusOK, Response{Logins: []Login{login}}) 1.474 +} 1.475 + 1.476 +func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.477 + var errors []RequestError 1.478 + vars := mux.Vars(r) 1.479 + if vars["login"] == "" { 1.480 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"}) 1.481 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.482 + return 1.483 + } 1.484 + var req LoginChange 1.485 + decoder := json.NewDecoder(r.Body) 1.486 + err := decoder.Decode(&req) 1.487 + if err != nil { 1.488 + log.Printf("Error decoding request: %#+v\n", err) 1.489 + encode(w, r, http.StatusBadRequest, invalidFormatResponse) 1.490 + return 1.491 + } 1.492 + login, err := context.GetLogin(vars["login"]) 1.493 + if err != nil { 1.494 + if err == ErrLoginNotFound { 1.495 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) 1.496 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 1.497 + return 1.498 + } 1.499 + log.Printf("Error retrieving login: %#+v\n", err) 1.500 + encode(w, r, http.StatusInternalServerError, actOfGodResponse) 1.501 + return 1.502 + } 1.503 + if req.Verification != nil { 1.504 + err = context.VerifyLogin(vars["login"], *req.Verification) 1.505 + if err != nil { 1.506 + if err == ErrLoginNotFound { 1.507 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) 1.508 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 1.509 + return 1.510 + } else if err == ErrLoginVerificationInvalid { 1.511 + errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"}) 1.512 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.513 + return 1.514 + } 1.515 + log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err) 1.516 + encode(w, r, http.StatusInternalServerError, actOfGodResponse) 1.517 + return 1.518 + } 1.519 + login.Verified = true 1.520 + } else if req.ResendVerification != nil { 1.521 + if !*req.ResendVerification { 1.522 + errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"}) 1.523 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.524 + return 1.525 + } 1.526 + context.SendLoginVerification(login) 1.527 + } else { 1.528 + errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"}) 1.529 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 1.530 + return 1.531 + } 1.532 + encode(w, r, http.StatusOK, Response{Logins: []Login{login}}) 1.533 +}