auth

Paddy 2015-05-17 Parent:581c60f8dd23 Child:b0d1b3e39fc8

172:8ecb60d29b0d Go to Latest

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.

History
     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 +}