Make client use our auth(n/z) scheme.
Our auth(n/z) scheme can be loosely defined as "encrypted tokens that nginx
transforms into headers" and "scopes for bypassing ACL". Our Go client, which is
what we'll be using to have services communicate with each other, follows this
paradigm now by auto-injecting the headers we'll need to identify ourselves.
This will work behind our firewall, but will be useless for the rest of the
world, which will need to go through the nginx bastion that can strip the
headers and replace them with the headers appropriate to the token attached to
the request.
This did involve setting a static client ID as the client for our
email_verification listener. Ideally, this would cause Client registration
(using that ID) when the listener starts up, ignoring ErrClientAlreadyExists. I
don't want to have to write the code that will allow us to bypass the Client ACL
properly right now, though, so we're just going to have to remember to manually
create that Client. Or not. I don't think it will do any harm (outside the OAuth
flow) to be using a Client ID that doesn't actually point to a Client. I just
think it'd be good for record-keeping purposes.
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")
68 ScopeLoginAdmin = Scope{ID: "login_admin", Name: "Administer Logins", Description: "Read and write logins, bypassing ACL."}
71 // Profile represents a single user of the service,
72 // including their authentication information.
74 ID uuid.ID `json:"id,omitempty"`
75 Name string `json:"name,omitempty"`
76 Passphrase string `json:"-"`
77 Iterations int `json:"-"`
78 Salt string `json:"-"`
79 PassphraseScheme int `json:"-"`
80 Compromised bool `json:"-"`
81 LockedUntil time.Time `json:"-"`
82 PassphraseReset string `json:"-"`
83 PassphraseResetCreated time.Time `json:"-"`
84 Created time.Time `json:"created,omitempty"`
85 LastSeen time.Time `json:"last_seen,omitempty"`
88 // ApplyChange applies the properties of the passed ProfileChange
89 // to the Profile it is called on.
90 func (p *Profile) ApplyChange(change ProfileChange) {
91 if change.Name != nil {
94 if change.Passphrase != nil {
95 p.Passphrase = *change.Passphrase
97 if change.Iterations != nil {
98 p.Iterations = *change.Iterations
100 if change.Salt != nil {
101 p.Salt = *change.Salt
103 if change.PassphraseScheme != nil {
104 p.PassphraseScheme = *change.PassphraseScheme
106 if change.Compromised != nil {
107 p.Compromised = *change.Compromised
109 if change.LockedUntil != nil {
110 p.LockedUntil = *change.LockedUntil
112 if change.PassphraseReset != nil {
113 p.PassphraseReset = *change.PassphraseReset
115 if change.PassphraseResetCreated != nil {
116 p.PassphraseResetCreated = *change.PassphraseResetCreated
118 if change.LastSeen != nil {
119 p.LastSeen = *change.LastSeen
123 // ApplyBulkChange applies the properties of the passed BulkProfileChange
124 // to the Profile it is called on.
125 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
126 if change.Compromised != nil {
127 p.Compromised = *change.Compromised
131 // ProfileChange represents a single atomic change to a Profile's mutable data.
132 type ProfileChange struct {
137 PassphraseScheme *int
139 LockedUntil *time.Time
140 PassphraseReset *string
141 PassphraseResetCreated *time.Time
145 func (c ProfileChange) Empty() bool {
146 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)
149 // Validate checks the ProfileChange it is called on
150 // and asserts its internal validity, or lack thereof.
151 // A descriptive error will be returned in the case of
152 // an invalid change.
153 func (c ProfileChange) Validate() error {
155 return ErrEmptyChange
157 if c.PassphraseScheme != nil && c.Passphrase == nil {
158 return ErrMissingPassphrase
160 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
161 return ErrMissingPassphraseResetCreated
163 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
164 return ErrMissingPassphraseReset
166 if c.Salt != nil && c.Passphrase == nil {
167 return ErrMissingPassphrase
169 if c.Iterations != nil && c.Passphrase == nil {
170 return ErrMissingPassphrase
175 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
176 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
177 // ProfileChange values across many Profiles all at once.
178 type BulkProfileChange struct {
182 func (b BulkProfileChange) Empty() bool {
183 return b.Compromised == nil
186 // Validate checks the BulkProfileChange it is called on
187 // and asserts its internal validity, or lack thereof.
188 // A descriptive error will be returned in the case of an
190 func (b BulkProfileChange) Validate() error {
192 return ErrEmptyChange
197 // Login represents a single human-friendly identifier for
198 // a given Profile that can be used to log into that Profile.
199 // Each Profile may only have one Login for each Type.
201 Type string `json:"type,omitempty"`
202 Value string `json:"value,omitempty"`
203 ProfileID uuid.ID `json:"profile_id,omitempty"`
204 Created time.Time `json:"created,omitempty"`
205 LastUsed time.Time `json:"last_used,omitempty"`
206 Verification string `json:"-"`
207 Verified bool `json:"verified"`
210 type LoginChange struct {
211 Verification *string `json:"verification,omitempty"`
212 ResendVerification *bool `json:"resend_verification,omitempty"`
215 type newProfileRequest struct {
216 Email string `json:"email"`
217 Passphrase string `json:"passphrase"`
218 Name string `json:"name"`
221 func validateNewProfileRequest(req *newProfileRequest) []RequestError {
222 errors := []RequestError{}
223 req.Name = strings.TrimSpace(req.Name)
224 req.Email = strings.TrimSpace(req.Email)
225 if len(req.Passphrase) < MinPassphraseLength {
226 errors = append(errors, RequestError{
227 Slug: RequestErrInsufficient,
228 Field: "/passphrase",
231 if len(req.Passphrase) > MaxPassphraseLength {
232 errors = append(errors, RequestError{
233 Slug: RequestErrOverflow,
234 Field: "/passphrase",
237 if len(req.Name) > MaxNameLength {
238 errors = append(errors, RequestError{
239 Slug: RequestErrOverflow,
244 errors = append(errors, RequestError{
245 Slug: RequestErrMissing,
249 if len(req.Email) > MaxEmailLength {
250 errors = append(errors, RequestError{
251 Slug: RequestErrOverflow,
255 re := regexp.MustCompile(".+@.+\\..+")
256 if !re.Match([]byte(req.Email)) {
257 errors = append(errors, RequestError{
258 Slug: RequestErrInvalidFormat,
265 type profileStore interface {
266 getProfileByID(id uuid.ID) (Profile, error)
267 getProfileByLogin(value string) (Profile, error)
268 saveProfile(profile Profile) error
269 updateProfile(id uuid.ID, change ProfileChange) error
270 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
271 deleteProfile(id uuid.ID) error
273 addLogin(login Login) error
274 getLogin(value string) (Login, error)
275 removeLogin(value string, profile uuid.ID) error
276 removeLoginsByProfile(profile uuid.ID) error
277 recordLoginUse(value string, when time.Time) error
278 verifyLogin(value, verification string) error
279 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
282 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
283 m.profileLock.RLock()
284 defer m.profileLock.RUnlock()
285 p, ok := m.profiles[id.String()]
287 return Profile{}, ErrProfileNotFound
292 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
294 defer m.loginLock.RUnlock()
295 login, ok := m.logins[value]
297 return Profile{}, ErrLoginNotFound
299 m.profileLock.RLock()
300 defer m.profileLock.RUnlock()
301 profile, ok := m.profiles[login.ProfileID.String()]
303 return Profile{}, ErrProfileNotFound
308 func (m *memstore) saveProfile(profile Profile) error {
310 defer m.profileLock.Unlock()
311 _, ok := m.profiles[profile.ID.String()]
313 return ErrProfileAlreadyExists
315 m.profiles[profile.ID.String()] = profile
319 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
321 defer m.profileLock.Unlock()
322 p, ok := m.profiles[id.String()]
324 return ErrProfileNotFound
326 p.ApplyChange(change)
327 m.profiles[id.String()] = p
331 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
333 defer m.profileLock.Unlock()
334 for id, profile := range m.profiles {
335 for _, i := range ids {
336 if id == i.String() {
337 profile.ApplyBulkChange(change)
338 m.profiles[id] = profile
346 func (m *memstore) deleteProfile(id uuid.ID) error {
348 defer m.profileLock.Unlock()
349 if _, ok := m.profiles[id.String()]; !ok {
350 return ErrProfileNotFound
352 delete(m.profiles, id.String())
356 func (m *memstore) addLogin(login Login) error {
358 defer m.loginLock.Unlock()
359 _, ok := m.logins[login.Value]
361 return ErrLoginAlreadyExists
363 m.logins[login.Value] = login
364 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
368 func (m *memstore) getLogin(value string) (Login, error) {
370 defer m.loginLock.RUnlock()
371 l, ok := m.logins[value]
373 return Login{}, ErrLoginNotFound
378 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
380 defer m.loginLock.Unlock()
381 l, ok := m.logins[value]
383 return ErrLoginNotFound
385 if !l.ProfileID.Equal(profile) {
386 return ErrLoginNotFound
388 delete(m.logins, value)
390 for p, id := range m.profileLoginLookup[profile.String()] {
397 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
402 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
404 defer m.loginLock.Unlock()
405 logins, ok := m.profileLoginLookup[profile.String()]
407 return ErrProfileNotFound
409 delete(m.profileLoginLookup, profile.String())
410 for _, login := range logins {
411 delete(m.logins, login)
416 func (m *memstore) recordLoginUse(value string, when time.Time) error {
418 defer m.loginLock.Unlock()
419 l, ok := m.logins[value]
421 return ErrLoginNotFound
428 func (m *memstore) verifyLogin(value, verification string) error {
430 defer m.loginLock.Unlock()
431 l, ok := m.logins[value]
433 return ErrLoginNotFound
435 if l.Verification != verification {
436 return ErrLoginVerificationInvalid
443 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
445 defer m.loginLock.RUnlock()
446 ids, ok := m.profileLoginLookup[profile.String()]
448 return []Login{}, nil
450 if len(ids) > num+offset {
451 ids = ids[offset : num+offset]
452 } else if len(ids) > offset {
455 return []Login{}, nil
458 for _, id := range ids {
459 login, ok := m.logins[id]
463 logins = append(logins, login)
468 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
469 err := context.RemoveLoginsByProfile(profile)
471 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
473 err = context.TerminateSessionsByProfile(profile)
475 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
477 err = context.RevokeTokensByProfileID(profile)
479 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
481 err = context.DeleteAuthorizationCodesByProfileID(profile)
483 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
485 clients, err := context.ListClientsByOwner(profile, -1, 0)
487 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
489 err = context.DeleteClientsByOwner(profile)
491 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
493 for _, client := range clients {
494 cleanUpAfterClientDeletion(client.ID, context)
498 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
499 func RegisterProfileHandlers(r *mux.Router, context Context) {
500 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
501 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
502 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
503 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
504 // BUG(paddy): We need to implement a handler that will add a login to a profile.
505 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
506 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
507 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS")
508 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS")
511 // GetProfileHandler is an HTTP handler for retrieving a profile.
512 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
513 errors := []RequestError{}
514 authz := r.Header.Get("Authorization")
515 if !strings.HasPrefix(authz, "Bearer ") {
516 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
517 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
520 authz = strings.TrimPrefix(authz, "Bearer ")
522 if vars["id"] == "" {
523 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
524 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
527 id, err := uuid.Parse(vars["id"])
529 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
530 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
533 token, err := context.GetToken(authz, false)
534 if err != nil || token.Revoked {
535 if err == ErrTokenNotFound || token.Revoked {
536 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
537 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
540 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
544 if !id.Equal(token.ProfileID) {
545 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
546 encode(w, r, http.StatusForbidden, Response{Errors: errors})
549 profile, err := context.GetProfileByID(id)
551 if err == ErrProfileNotFound {
552 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
553 encode(w, r, http.StatusNotFound, Response{Errors: errors})
556 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
559 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
563 // CreateProfileHandler is an HTTP handler for registering new profiles.
564 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
565 scheme, ok := passphraseSchemes[CurPassphraseScheme]
567 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
568 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
571 var req newProfileRequest
572 errors := []RequestError{}
573 decoder := json.NewDecoder(r.Body)
574 err := decoder.Decode(&req)
576 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
579 errors = append(errors, validateNewProfileRequest(&req)...)
581 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
584 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
586 log.Printf("Error creating encoded passphrase: %#+v\n", err)
587 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
593 Passphrase: string(passphrase),
594 Iterations: context.config.iterations,
596 PassphraseScheme: CurPassphraseScheme,
598 LastSeen: time.Now(),
600 err = context.SaveProfile(profile)
602 if err == ErrProfileAlreadyExists {
603 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
606 log.Printf("Error saving profile: %#+v\n", err)
607 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
614 Created: profile.Created,
615 LastUsed: profile.Created,
616 ProfileID: profile.ID,
617 Verification: uuid.NewID().String(),
619 err = context.AddLogin(login)
621 if err == ErrLoginAlreadyExists {
622 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
625 log.Printf("Error adding login: %#+v\n", err)
626 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
629 logins = append(logins, login)
632 Profiles: []Profile{profile},
634 encode(w, r, http.StatusCreated, resp)
635 go context.SendLoginVerification(login)
638 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
639 errors := []RequestError{}
641 if vars["id"] == "" {
642 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
643 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
646 id, err := uuid.Parse(vars["id"])
648 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
649 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
652 username, password, ok := r.BasicAuth()
654 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
655 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
658 profile, err := authenticate(username, password, context)
660 if isAuthError(err) {
661 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
662 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
664 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
665 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
669 if !profile.ID.Equal(id) {
670 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
671 encode(w, r, http.StatusForbidden, Response{Errors: errors})
674 var req ProfileChange
675 decoder := json.NewDecoder(r.Body)
676 err = decoder.Decode(&req)
678 log.Printf("Error decoding request: %#+v\n", err)
679 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
684 req.PassphraseScheme = nil
685 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
686 req.LockedUntil = nil
688 if req.Passphrase != nil {
689 if len(*req.Passphrase) < MinPassphraseLength {
690 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
691 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
694 if len(*req.Passphrase) > MaxPassphraseLength {
695 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
696 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
699 iterations := context.config.iterations
700 scheme, ok := passphraseSchemes[CurPassphraseScheme]
702 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
705 curScheme := CurPassphraseScheme
706 req.PassphraseScheme = &curScheme
707 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
709 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
712 req.Passphrase = &passphrase
714 req.Iterations = &iterations
716 if req.PassphraseReset != nil {
718 req.PassphraseResetCreated = &now
726 resp.Profiles = []Profile{profile}
727 status = http.StatusOK
729 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
731 status = http.StatusInternalServerError
733 encode(w, r, status, resp)
736 err = context.UpdateProfile(id, req)
738 if err == ErrProfileNotFound {
739 errors = append(errors, RequestError{Slug: RequestErrNotFound})
740 encode(w, r, http.StatusNotFound, Response{Errors: errors})
743 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
746 profile.ApplyChange(req)
747 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
751 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
752 errors := []RequestError{}
754 if vars["id"] == "" {
755 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
756 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
759 id, err := uuid.Parse(vars["id"])
761 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
762 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
765 username, password, ok := r.BasicAuth()
767 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
768 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
771 profile, err := authenticate(username, password, context)
773 if isAuthError(err) {
774 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
775 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
777 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
778 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
782 if !profile.ID.Equal(id) {
783 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
784 encode(w, r, http.StatusForbidden, Response{Errors: errors})
787 err = context.DeleteProfile(id)
789 if err == ErrProfileNotFound {
790 errors = append(errors, RequestError{Slug: RequestErrNotFound})
791 encode(w, r, http.StatusNotFound, Response{Errors: errors})
794 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
797 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
798 go cleanUpAfterProfileDeletion(profile.ID, context)
801 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
802 var errors []RequestError
804 if vars["login"] == "" {
805 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
806 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
809 login, err := context.GetLogin(vars["login"])
811 if err == ErrLoginNotFound {
812 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
813 encode(w, r, http.StatusNotFound, Response{Errors: errors})
816 log.Printf("Error retrieving login: %#+v\n", err)
817 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
820 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
823 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
824 var errors []RequestError
826 if vars["login"] == "" {
827 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
828 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
832 decoder := json.NewDecoder(r.Body)
833 err := decoder.Decode(&req)
835 log.Printf("Error decoding request: %#+v\n", err)
836 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
839 login, err := context.GetLogin(vars["login"])
841 if err == ErrLoginNotFound {
842 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
843 encode(w, r, http.StatusNotFound, Response{Errors: errors})
846 log.Printf("Error retrieving login: %#+v\n", err)
847 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
850 if req.Verification != nil {
851 err = context.VerifyLogin(vars["login"], *req.Verification)
853 if err == ErrLoginNotFound {
854 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
855 encode(w, r, http.StatusNotFound, Response{Errors: errors})
857 } else if err == ErrLoginVerificationInvalid {
858 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
859 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
862 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
863 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
866 login.Verified = true
867 } else if req.ResendVerification != nil {
868 if !*req.ResendVerification {
869 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"})
870 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
873 context.SendLoginVerification(login)
875 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
876 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
879 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})