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.
12 "code.secondbit.org/uuid.hg"
13 "github.com/gorilla/mux"
17 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
18 MinPassphraseLength = 6
19 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
20 MaxPassphraseLength = 64
21 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
22 CurPassphraseScheme = 1
23 // MaxNameLength is the maximum length, in bytes, of a name, exclusive.
25 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
30 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
31 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
32 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
33 // the same ID already exists in the profileStore.
34 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
35 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
36 ErrProfileNotFound = errors.New("profile not found in profileStore")
37 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
38 // Type and Value already exists in the profileStore.
39 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
40 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
41 ErrLoginNotFound = errors.New("login not found in profileStore")
42 // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code.
43 ErrLoginVerificationInvalid = errors.New("login verification code incorrect")
45 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
46 // Passphrase, and requires one.
47 ErrMissingPassphrase = errors.New("missing passphrase")
48 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
49 // a PassphraseReset, and requires one.
50 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
51 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
52 // contain a PassphraseResetCreated, and requires one.
53 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
54 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
55 // but the Passphrase is shorter than MinPassphraseLength.
56 ErrPassphraseTooShort = errors.New("passphrase too short")
57 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
58 // but the Passphrase is longer than MaxPassphraseLength.
59 ErrPassphraseTooLong = errors.New("passphrase too long")
61 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
62 // of being compromised.
63 ErrProfileCompromised = errors.New("profile compromised")
64 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
65 // duration, to prevent brute force attacks.
66 ErrProfileLocked = errors.New("profile locked")
69 // Profile represents a single user of the service,
70 // including their authentication information.
72 ID uuid.ID `json:"id,omitempty"`
73 Name string `json:"name,omitempty"`
74 Passphrase string `json:"-"`
75 Iterations int `json:"-"`
76 Salt string `json:"-"`
77 PassphraseScheme int `json:"-"`
78 Compromised bool `json:"-"`
79 LockedUntil time.Time `json:"-"`
80 PassphraseReset string `json:"-"`
81 PassphraseResetCreated time.Time `json:"-"`
82 Created time.Time `json:"created,omitempty"`
83 LastSeen time.Time `json:"last_seen,omitempty"`
86 // ApplyChange applies the properties of the passed ProfileChange
87 // to the Profile it is called on.
88 func (p *Profile) ApplyChange(change ProfileChange) {
89 if change.Name != nil {
92 if change.Passphrase != nil {
93 p.Passphrase = *change.Passphrase
95 if change.Iterations != nil {
96 p.Iterations = *change.Iterations
98 if change.Salt != nil {
101 if change.PassphraseScheme != nil {
102 p.PassphraseScheme = *change.PassphraseScheme
104 if change.Compromised != nil {
105 p.Compromised = *change.Compromised
107 if change.LockedUntil != nil {
108 p.LockedUntil = *change.LockedUntil
110 if change.PassphraseReset != nil {
111 p.PassphraseReset = *change.PassphraseReset
113 if change.PassphraseResetCreated != nil {
114 p.PassphraseResetCreated = *change.PassphraseResetCreated
116 if change.LastSeen != nil {
117 p.LastSeen = *change.LastSeen
121 // ApplyBulkChange applies the properties of the passed BulkProfileChange
122 // to the Profile it is called on.
123 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
124 if change.Compromised != nil {
125 p.Compromised = *change.Compromised
129 // ProfileChange represents a single atomic change to a Profile's mutable data.
130 type ProfileChange struct {
135 PassphraseScheme *int
137 LockedUntil *time.Time
138 PassphraseReset *string
139 PassphraseResetCreated *time.Time
143 func (c ProfileChange) Empty() bool {
144 return (c.Name == nil && c.Passphrase == nil && c.Iterations == nil && c.Salt == nil && c.PassphraseScheme == nil && c.Compromised == nil && c.LockedUntil == nil && c.PassphraseReset == nil && c.PassphraseResetCreated == nil && c.LastSeen == nil)
147 // Validate checks the ProfileChange it is called on
148 // and asserts its internal validity, or lack thereof.
149 // A descriptive error will be returned in the case of
150 // an invalid change.
151 func (c ProfileChange) Validate() error {
153 return ErrEmptyChange
155 if c.PassphraseScheme != nil && c.Passphrase == nil {
156 return ErrMissingPassphrase
158 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
159 return ErrMissingPassphraseResetCreated
161 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
162 return ErrMissingPassphraseReset
164 if c.Salt != nil && c.Passphrase == nil {
165 return ErrMissingPassphrase
167 if c.Iterations != nil && c.Passphrase == nil {
168 return ErrMissingPassphrase
173 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
174 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
175 // ProfileChange values across many Profiles all at once.
176 type BulkProfileChange struct {
180 func (b BulkProfileChange) Empty() bool {
181 return b.Compromised == nil
184 // Validate checks the BulkProfileChange it is called on
185 // and asserts its internal validity, or lack thereof.
186 // A descriptive error will be returned in the case of an
188 func (b BulkProfileChange) Validate() error {
190 return ErrEmptyChange
195 // Login represents a single human-friendly identifier for
196 // a given Profile that can be used to log into that Profile.
197 // Each Profile may only have one Login for each Type.
199 Type string `json:"type,omitempty"`
200 Value string `json:"value,omitempty"`
201 ProfileID uuid.ID `json:"profile_id,omitempty"`
202 Created time.Time `json:"created,omitempty"`
203 LastUsed time.Time `json:"last_used,omitempty"`
204 Verification string `json:"-"`
205 Verified bool `json:"verified"`
208 type LoginChange struct {
209 Verification *string `json:"verification,omitempty"`
210 ResendVerification *bool `json:"resend_verification,omitempty"`
213 type newProfileRequest struct {
214 Email string `json:"email"`
215 Passphrase string `json:"passphrase"`
216 Name string `json:"name"`
219 func validateNewProfileRequest(req *newProfileRequest) []RequestError {
220 errors := []RequestError{}
221 req.Name = strings.TrimSpace(req.Name)
222 req.Email = strings.TrimSpace(req.Email)
223 if len(req.Passphrase) < MinPassphraseLength {
224 errors = append(errors, RequestError{
225 Slug: RequestErrInsufficient,
226 Field: "/passphrase",
229 if len(req.Passphrase) > MaxPassphraseLength {
230 errors = append(errors, RequestError{
231 Slug: RequestErrOverflow,
232 Field: "/passphrase",
235 if len(req.Name) > MaxNameLength {
236 errors = append(errors, RequestError{
237 Slug: RequestErrOverflow,
242 errors = append(errors, RequestError{
243 Slug: RequestErrMissing,
247 if len(req.Email) > MaxEmailLength {
248 errors = append(errors, RequestError{
249 Slug: RequestErrOverflow,
253 re := regexp.MustCompile(".+@.+\\..+")
254 if !re.Match([]byte(req.Email)) {
255 errors = append(errors, RequestError{
256 Slug: RequestErrInvalidFormat,
263 type profileStore interface {
264 getProfileByID(id uuid.ID) (Profile, error)
265 getProfileByLogin(value string) (Profile, error)
266 saveProfile(profile Profile) error
267 updateProfile(id uuid.ID, change ProfileChange) error
268 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
269 deleteProfile(id uuid.ID) error
271 addLogin(login Login) error
272 getLogin(value string) (Login, error)
273 removeLogin(value string, profile uuid.ID) error
274 removeLoginsByProfile(profile uuid.ID) error
275 recordLoginUse(value string, when time.Time) error
276 verifyLogin(value, verification string) error
277 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
280 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
281 m.profileLock.RLock()
282 defer m.profileLock.RUnlock()
283 p, ok := m.profiles[id.String()]
285 return Profile{}, ErrProfileNotFound
290 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
292 defer m.loginLock.RUnlock()
293 login, ok := m.logins[value]
295 return Profile{}, ErrLoginNotFound
297 m.profileLock.RLock()
298 defer m.profileLock.RUnlock()
299 profile, ok := m.profiles[login.ProfileID.String()]
301 return Profile{}, ErrProfileNotFound
306 func (m *memstore) saveProfile(profile Profile) error {
308 defer m.profileLock.Unlock()
309 _, ok := m.profiles[profile.ID.String()]
311 return ErrProfileAlreadyExists
313 m.profiles[profile.ID.String()] = profile
317 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
319 defer m.profileLock.Unlock()
320 p, ok := m.profiles[id.String()]
322 return ErrProfileNotFound
324 p.ApplyChange(change)
325 m.profiles[id.String()] = p
329 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
331 defer m.profileLock.Unlock()
332 for id, profile := range m.profiles {
333 for _, i := range ids {
334 if id == i.String() {
335 profile.ApplyBulkChange(change)
336 m.profiles[id] = profile
344 func (m *memstore) deleteProfile(id uuid.ID) error {
346 defer m.profileLock.Unlock()
347 if _, ok := m.profiles[id.String()]; !ok {
348 return ErrProfileNotFound
350 delete(m.profiles, id.String())
354 func (m *memstore) addLogin(login Login) error {
356 defer m.loginLock.Unlock()
357 _, ok := m.logins[login.Value]
359 return ErrLoginAlreadyExists
361 m.logins[login.Value] = login
362 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
366 func (m *memstore) getLogin(value string) (Login, error) {
368 defer m.loginLock.RUnlock()
369 l, ok := m.logins[value]
371 return Login{}, ErrLoginNotFound
376 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
378 defer m.loginLock.Unlock()
379 l, ok := m.logins[value]
381 return ErrLoginNotFound
383 if !l.ProfileID.Equal(profile) {
384 return ErrLoginNotFound
386 delete(m.logins, value)
388 for p, id := range m.profileLoginLookup[profile.String()] {
395 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
400 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
402 defer m.loginLock.Unlock()
403 logins, ok := m.profileLoginLookup[profile.String()]
405 return ErrProfileNotFound
407 delete(m.profileLoginLookup, profile.String())
408 for _, login := range logins {
409 delete(m.logins, login)
414 func (m *memstore) recordLoginUse(value string, when time.Time) error {
416 defer m.loginLock.Unlock()
417 l, ok := m.logins[value]
419 return ErrLoginNotFound
426 func (m *memstore) verifyLogin(value, verification string) error {
428 defer m.loginLock.Unlock()
429 l, ok := m.logins[value]
431 return ErrLoginNotFound
433 if l.Verification != verification {
434 return ErrLoginVerificationInvalid
441 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
443 defer m.loginLock.RUnlock()
444 ids, ok := m.profileLoginLookup[profile.String()]
446 return []Login{}, nil
448 if len(ids) > num+offset {
449 ids = ids[offset : num+offset]
450 } else if len(ids) > offset {
453 return []Login{}, nil
456 for _, id := range ids {
457 login, ok := m.logins[id]
461 logins = append(logins, login)
466 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
467 err := context.RemoveLoginsByProfile(profile)
469 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
471 err = context.TerminateSessionsByProfile(profile)
473 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
475 err = context.RevokeTokensByProfileID(profile)
477 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
479 err = context.DeleteAuthorizationCodesByProfileID(profile)
481 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
483 clients, err := context.ListClientsByOwner(profile, -1, 0)
485 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
487 err = context.DeleteClientsByOwner(profile)
489 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
491 for _, client := range clients {
492 cleanUpAfterClientDeletion(client.ID, context)
496 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
497 func RegisterProfileHandlers(r *mux.Router, context Context) {
498 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
499 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
500 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
501 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
502 // BUG(paddy): We need to implement a handler that will add a login to a profile.
503 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
504 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
505 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS")
506 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS")
509 // GetProfileHandler is an HTTP handler for retrieving a profile.
510 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
511 errors := []RequestError{}
512 authz := r.Header.Get("Authorization")
513 if !strings.HasPrefix(authz, "Bearer ") {
514 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
515 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
518 authz = strings.TrimPrefix(authz, "Bearer ")
520 if vars["id"] == "" {
521 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
522 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
525 id, err := uuid.Parse(vars["id"])
527 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
528 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
531 token, err := context.GetToken(authz, false)
532 if err != nil || token.Revoked {
533 if err == ErrTokenNotFound || token.Revoked {
534 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
535 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
538 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
542 if !id.Equal(token.ProfileID) {
543 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
544 encode(w, r, http.StatusForbidden, Response{Errors: errors})
547 profile, err := context.GetProfileByID(id)
549 if err == ErrProfileNotFound {
550 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
551 encode(w, r, http.StatusNotFound, Response{Errors: errors})
554 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
557 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
561 // CreateProfileHandler is an HTTP handler for registering new profiles.
562 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
563 scheme, ok := passphraseSchemes[CurPassphraseScheme]
565 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
566 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
569 var req newProfileRequest
570 errors := []RequestError{}
571 decoder := json.NewDecoder(r.Body)
572 err := decoder.Decode(&req)
574 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
577 errors = append(errors, validateNewProfileRequest(&req)...)
579 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
582 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
584 log.Printf("Error creating encoded passphrase: %#+v\n", err)
585 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
591 Passphrase: string(passphrase),
592 Iterations: context.config.iterations,
594 PassphraseScheme: CurPassphraseScheme,
596 LastSeen: time.Now(),
598 err = context.SaveProfile(profile)
600 if err == ErrProfileAlreadyExists {
601 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
604 log.Printf("Error saving profile: %#+v\n", err)
605 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
612 Created: profile.Created,
613 LastUsed: profile.Created,
614 ProfileID: profile.ID,
615 Verification: uuid.NewID().String(),
617 err = context.AddLogin(login)
619 if err == ErrLoginAlreadyExists {
620 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
623 log.Printf("Error adding login: %#+v\n", err)
624 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
627 logins = append(logins, login)
630 Profiles: []Profile{profile},
632 encode(w, r, http.StatusCreated, resp)
633 go context.SendLoginVerification(login)
636 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
637 errors := []RequestError{}
639 if vars["id"] == "" {
640 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
641 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
644 id, err := uuid.Parse(vars["id"])
646 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
647 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
650 username, password, ok := r.BasicAuth()
652 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
653 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
656 profile, err := authenticate(username, password, context)
658 if isAuthError(err) {
659 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
660 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
662 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
663 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
667 if !profile.ID.Equal(id) {
668 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
669 encode(w, r, http.StatusForbidden, Response{Errors: errors})
672 var req ProfileChange
673 decoder := json.NewDecoder(r.Body)
674 err = decoder.Decode(&req)
676 log.Printf("Error decoding request: %#+v\n", err)
677 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
682 req.PassphraseScheme = nil
683 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
684 req.LockedUntil = nil
686 if req.Passphrase != nil {
687 if len(*req.Passphrase) < MinPassphraseLength {
688 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
689 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
692 if len(*req.Passphrase) > MaxPassphraseLength {
693 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
694 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
697 iterations := context.config.iterations
698 scheme, ok := passphraseSchemes[CurPassphraseScheme]
700 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
703 curScheme := CurPassphraseScheme
704 req.PassphraseScheme = &curScheme
705 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
707 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
710 req.Passphrase = &passphrase
712 req.Iterations = &iterations
714 if req.PassphraseReset != nil {
716 req.PassphraseResetCreated = &now
724 resp.Profiles = []Profile{profile}
725 status = http.StatusOK
727 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
729 status = http.StatusInternalServerError
731 encode(w, r, status, resp)
734 err = context.UpdateProfile(id, req)
736 if err == ErrProfileNotFound {
737 errors = append(errors, RequestError{Slug: RequestErrNotFound})
738 encode(w, r, http.StatusNotFound, Response{Errors: errors})
741 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
744 profile.ApplyChange(req)
745 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
749 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
750 errors := []RequestError{}
752 if vars["id"] == "" {
753 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
754 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
757 id, err := uuid.Parse(vars["id"])
759 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
760 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
763 username, password, ok := r.BasicAuth()
765 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
766 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
769 profile, err := authenticate(username, password, context)
771 if isAuthError(err) {
772 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
773 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
775 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
776 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
780 if !profile.ID.Equal(id) {
781 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
782 encode(w, r, http.StatusForbidden, Response{Errors: errors})
785 err = context.DeleteProfile(id)
787 if err == ErrProfileNotFound {
788 errors = append(errors, RequestError{Slug: RequestErrNotFound})
789 encode(w, r, http.StatusNotFound, Response{Errors: errors})
792 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
795 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
796 go cleanUpAfterProfileDeletion(profile.ID, context)
799 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
800 var errors []RequestError
802 if vars["login"] == "" {
803 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
804 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
807 login, err := context.GetLogin(vars["login"])
809 if err == ErrLoginNotFound {
810 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
811 encode(w, r, http.StatusNotFound, Response{Errors: errors})
814 log.Printf("Error retrieving login: %#+v\n", err)
815 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
818 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
821 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
822 var errors []RequestError
824 if vars["login"] == "" {
825 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
826 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
830 decoder := json.NewDecoder(r.Body)
831 err := decoder.Decode(&req)
833 log.Printf("Error decoding request: %#+v\n", err)
834 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
837 login, err := context.GetLogin(vars["login"])
839 if err == ErrLoginNotFound {
840 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
841 encode(w, r, http.StatusNotFound, Response{Errors: errors})
844 log.Printf("Error retrieving login: %#+v\n", err)
845 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
848 if req.Verification != nil {
849 err = context.VerifyLogin(vars["login"], *req.Verification)
851 if err == ErrLoginNotFound {
852 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
853 encode(w, r, http.StatusNotFound, Response{Errors: errors})
855 } else if err == ErrLoginVerificationInvalid {
856 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
857 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
860 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
861 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
864 login.Verified = true
865 } else if req.ResendVerification != nil {
866 if !*req.ResendVerification {
867 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"})
868 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
871 context.SendLoginVerification(login)
873 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
874 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
877 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})