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.
| paddy@27 | 1 package auth |
| paddy@27 | 2 |
| paddy@27 | 3 import ( |
| paddy@99 | 4 "encoding/json" |
| paddy@38 | 5 "errors" |
| paddy@149 | 6 "log" |
| paddy@99 | 7 "net/http" |
| paddy@99 | 8 "regexp" |
| paddy@99 | 9 "strings" |
| paddy@27 | 10 "time" |
| paddy@27 | 11 |
| paddy@107 | 12 "code.secondbit.org/uuid.hg" |
| paddy@105 | 13 "github.com/gorilla/mux" |
| paddy@27 | 14 ) |
| paddy@27 | 15 |
| paddy@48 | 16 const ( |
| paddy@57 | 17 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 18 MinPassphraseLength = 6 |
| paddy@57 | 19 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 20 MaxPassphraseLength = 64 |
| paddy@69 | 21 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme |
| paddy@69 | 22 CurPassphraseScheme = 1 |
| paddy@99 | 23 // MaxNameLength is the maximum length, in bytes, of a name, exclusive. |
| paddy@99 | 24 MaxNameLength = 64 |
| paddy@99 | 25 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive. |
| paddy@99 | 26 MaxEmailLength = 64 |
| paddy@48 | 27 ) |
| paddy@48 | 28 |
| paddy@38 | 29 var ( |
| paddy@57 | 30 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first. |
| paddy@57 | 31 ErrNoProfileStore = errors.New("no profileStore was specified for the Context") |
| paddy@57 | 32 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with |
| paddy@57 | 33 // the same ID already exists in the profileStore. |
| paddy@57 | 34 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore") |
| paddy@57 | 35 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore. |
| paddy@57 | 36 ErrProfileNotFound = errors.New("profile not found in profileStore") |
| paddy@57 | 37 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same |
| paddy@57 | 38 // Type and Value already exists in the profileStore. |
| paddy@57 | 39 ErrLoginAlreadyExists = errors.New("login already exists in profileStore") |
| paddy@57 | 40 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore. |
| paddy@57 | 41 ErrLoginNotFound = errors.New("login not found in profileStore") |
| paddy@172 | 42 // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code. |
| paddy@172 | 43 ErrLoginVerificationInvalid = errors.New("login verification code incorrect") |
| paddy@48 | 44 |
| paddy@57 | 45 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a |
| paddy@57 | 46 // Passphrase, and requires one. |
| paddy@57 | 47 ErrMissingPassphrase = errors.New("missing passphrase") |
| paddy@57 | 48 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain |
| paddy@57 | 49 // a PassphraseReset, and requires one. |
| paddy@57 | 50 ErrMissingPassphraseReset = errors.New("missing passphrase reset") |
| paddy@57 | 51 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not |
| paddy@57 | 52 // contain a PassphraseResetCreated, and requires one. |
| paddy@48 | 53 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp") |
| paddy@57 | 54 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 55 // but the Passphrase is shorter than MinPassphraseLength. |
| paddy@57 | 56 ErrPassphraseTooShort = errors.New("passphrase too short") |
| paddy@57 | 57 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 58 // but the Passphrase is longer than MaxPassphraseLength. |
| paddy@57 | 59 ErrPassphraseTooLong = errors.New("passphrase too long") |
| paddy@99 | 60 |
| paddy@99 | 61 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected |
| paddy@99 | 62 // of being compromised. |
| paddy@99 | 63 ErrProfileCompromised = errors.New("profile compromised") |
| paddy@99 | 64 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain |
| paddy@99 | 65 // duration, to prevent brute force attacks. |
| paddy@99 | 66 ErrProfileLocked = errors.New("profile locked") |
| paddy@38 | 67 ) |
| paddy@38 | 68 |
| paddy@57 | 69 // Profile represents a single user of the service, |
| paddy@158 | 70 // including their authentication information. |
| paddy@27 | 71 type Profile struct { |
| paddy@105 | 72 ID uuid.ID `json:"id,omitempty"` |
| paddy@105 | 73 Name string `json:"name,omitempty"` |
| paddy@105 | 74 Passphrase string `json:"-"` |
| paddy@105 | 75 Iterations int `json:"-"` |
| paddy@105 | 76 Salt string `json:"-"` |
| paddy@105 | 77 PassphraseScheme int `json:"-"` |
| paddy@105 | 78 Compromised bool `json:"-"` |
| paddy@105 | 79 LockedUntil time.Time `json:"-"` |
| paddy@105 | 80 PassphraseReset string `json:"-"` |
| paddy@105 | 81 PassphraseResetCreated time.Time `json:"-"` |
| paddy@105 | 82 Created time.Time `json:"created,omitempty"` |
| paddy@105 | 83 LastSeen time.Time `json:"last_seen,omitempty"` |
| paddy@38 | 84 } |
| paddy@38 | 85 |
| paddy@57 | 86 // ApplyChange applies the properties of the passed ProfileChange |
| paddy@57 | 87 // to the Profile it is called on. |
| paddy@38 | 88 func (p *Profile) ApplyChange(change ProfileChange) { |
| paddy@38 | 89 if change.Name != nil { |
| paddy@38 | 90 p.Name = *change.Name |
| paddy@38 | 91 } |
| paddy@38 | 92 if change.Passphrase != nil { |
| paddy@38 | 93 p.Passphrase = *change.Passphrase |
| paddy@38 | 94 } |
| paddy@38 | 95 if change.Iterations != nil { |
| paddy@38 | 96 p.Iterations = *change.Iterations |
| paddy@38 | 97 } |
| paddy@38 | 98 if change.Salt != nil { |
| paddy@38 | 99 p.Salt = *change.Salt |
| paddy@38 | 100 } |
| paddy@38 | 101 if change.PassphraseScheme != nil { |
| paddy@38 | 102 p.PassphraseScheme = *change.PassphraseScheme |
| paddy@38 | 103 } |
| paddy@38 | 104 if change.Compromised != nil { |
| paddy@38 | 105 p.Compromised = *change.Compromised |
| paddy@38 | 106 } |
| paddy@38 | 107 if change.LockedUntil != nil { |
| paddy@38 | 108 p.LockedUntil = *change.LockedUntil |
| paddy@38 | 109 } |
| paddy@38 | 110 if change.PassphraseReset != nil { |
| paddy@38 | 111 p.PassphraseReset = *change.PassphraseReset |
| paddy@38 | 112 } |
| paddy@38 | 113 if change.PassphraseResetCreated != nil { |
| paddy@38 | 114 p.PassphraseResetCreated = *change.PassphraseResetCreated |
| paddy@38 | 115 } |
| paddy@38 | 116 if change.LastSeen != nil { |
| paddy@38 | 117 p.LastSeen = *change.LastSeen |
| paddy@38 | 118 } |
| paddy@38 | 119 } |
| paddy@38 | 120 |
| paddy@57 | 121 // ApplyBulkChange applies the properties of the passed BulkProfileChange |
| paddy@57 | 122 // to the Profile it is called on. |
| paddy@44 | 123 func (p *Profile) ApplyBulkChange(change BulkProfileChange) { |
| paddy@44 | 124 if change.Compromised != nil { |
| paddy@44 | 125 p.Compromised = *change.Compromised |
| paddy@44 | 126 } |
| paddy@44 | 127 } |
| paddy@44 | 128 |
| paddy@57 | 129 // ProfileChange represents a single atomic change to a Profile's mutable data. |
| paddy@38 | 130 type ProfileChange struct { |
| paddy@38 | 131 Name *string |
| paddy@38 | 132 Passphrase *string |
| paddy@69 | 133 Iterations *int |
| paddy@38 | 134 Salt *string |
| paddy@38 | 135 PassphraseScheme *int |
| paddy@38 | 136 Compromised *bool |
| paddy@38 | 137 LockedUntil *time.Time |
| paddy@38 | 138 PassphraseReset *string |
| paddy@38 | 139 PassphraseResetCreated *time.Time |
| paddy@38 | 140 LastSeen *time.Time |
| paddy@38 | 141 } |
| paddy@38 | 142 |
| paddy@149 | 143 func (c ProfileChange) Empty() bool { |
| paddy@161 | 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) |
| paddy@149 | 145 } |
| paddy@149 | 146 |
| paddy@57 | 147 // Validate checks the ProfileChange it is called on |
| paddy@57 | 148 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 149 // A descriptive error will be returned in the case of |
| paddy@57 | 150 // an invalid change. |
| paddy@38 | 151 func (c ProfileChange) Validate() error { |
| paddy@149 | 152 if c.Empty() { |
| paddy@48 | 153 return ErrEmptyChange |
| paddy@48 | 154 } |
| paddy@48 | 155 if c.PassphraseScheme != nil && c.Passphrase == nil { |
| paddy@48 | 156 return ErrMissingPassphrase |
| paddy@48 | 157 } |
| paddy@48 | 158 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil { |
| paddy@48 | 159 return ErrMissingPassphraseResetCreated |
| paddy@48 | 160 } |
| paddy@48 | 161 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil { |
| paddy@48 | 162 return ErrMissingPassphraseReset |
| paddy@48 | 163 } |
| paddy@48 | 164 if c.Salt != nil && c.Passphrase == nil { |
| paddy@48 | 165 return ErrMissingPassphrase |
| paddy@48 | 166 } |
| paddy@48 | 167 if c.Iterations != nil && c.Passphrase == nil { |
| paddy@48 | 168 return ErrMissingPassphrase |
| paddy@48 | 169 } |
| paddy@38 | 170 return nil |
| paddy@27 | 171 } |
| paddy@27 | 172 |
| paddy@57 | 173 // BulkProfileChange represents a single atomic change to many Profiles' mutable data. |
| paddy@57 | 174 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the |
| paddy@57 | 175 // ProfileChange values across many Profiles all at once. |
| paddy@44 | 176 type BulkProfileChange struct { |
| paddy@44 | 177 Compromised *bool |
| paddy@44 | 178 } |
| paddy@44 | 179 |
| paddy@149 | 180 func (b BulkProfileChange) Empty() bool { |
| paddy@149 | 181 return b.Compromised == nil |
| paddy@149 | 182 } |
| paddy@149 | 183 |
| paddy@57 | 184 // Validate checks the BulkProfileChange it is called on |
| paddy@57 | 185 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 186 // A descriptive error will be returned in the case of an |
| paddy@57 | 187 // invalid change. |
| paddy@44 | 188 func (b BulkProfileChange) Validate() error { |
| paddy@149 | 189 if b.Empty() { |
| paddy@48 | 190 return ErrEmptyChange |
| paddy@48 | 191 } |
| paddy@44 | 192 return nil |
| paddy@44 | 193 } |
| paddy@44 | 194 |
| paddy@57 | 195 // Login represents a single human-friendly identifier for |
| paddy@57 | 196 // a given Profile that can be used to log into that Profile. |
| paddy@57 | 197 // Each Profile may only have one Login for each Type. |
| paddy@27 | 198 type Login struct { |
| paddy@172 | 199 Type string `json:"type,omitempty"` |
| paddy@172 | 200 Value string `json:"value,omitempty"` |
| paddy@172 | 201 ProfileID uuid.ID `json:"profile_id,omitempty"` |
| paddy@172 | 202 Created time.Time `json:"created,omitempty"` |
| paddy@172 | 203 LastUsed time.Time `json:"last_used,omitempty"` |
| paddy@172 | 204 Verification string `json:"-"` |
| paddy@172 | 205 Verified bool `json:"verified"` |
| paddy@172 | 206 } |
| paddy@172 | 207 |
| paddy@172 | 208 type LoginChange struct { |
| paddy@172 | 209 Verification *string `json:"verification,omitempty"` |
| paddy@172 | 210 ResendVerification *bool `json:"resend_verification,omitempty"` |
| paddy@27 | 211 } |
| paddy@27 | 212 |
| paddy@99 | 213 type newProfileRequest struct { |
| paddy@99 | 214 Email string `json:"email"` |
| paddy@99 | 215 Passphrase string `json:"passphrase"` |
| paddy@99 | 216 Name string `json:"name"` |
| paddy@99 | 217 } |
| paddy@99 | 218 |
| paddy@172 | 219 func validateNewProfileRequest(req *newProfileRequest) []RequestError { |
| paddy@172 | 220 errors := []RequestError{} |
| paddy@99 | 221 req.Name = strings.TrimSpace(req.Name) |
| paddy@99 | 222 req.Email = strings.TrimSpace(req.Email) |
| paddy@99 | 223 if len(req.Passphrase) < MinPassphraseLength { |
| paddy@172 | 224 errors = append(errors, RequestError{ |
| paddy@172 | 225 Slug: RequestErrInsufficient, |
| paddy@99 | 226 Field: "/passphrase", |
| paddy@99 | 227 }) |
| paddy@99 | 228 } |
| paddy@99 | 229 if len(req.Passphrase) > MaxPassphraseLength { |
| paddy@172 | 230 errors = append(errors, RequestError{ |
| paddy@172 | 231 Slug: RequestErrOverflow, |
| paddy@99 | 232 Field: "/passphrase", |
| paddy@99 | 233 }) |
| paddy@99 | 234 } |
| paddy@99 | 235 if len(req.Name) > MaxNameLength { |
| paddy@172 | 236 errors = append(errors, RequestError{ |
| paddy@172 | 237 Slug: RequestErrOverflow, |
| paddy@99 | 238 Field: "/name", |
| paddy@99 | 239 }) |
| paddy@99 | 240 } |
| paddy@99 | 241 if req.Email == "" { |
| paddy@172 | 242 errors = append(errors, RequestError{ |
| paddy@172 | 243 Slug: RequestErrMissing, |
| paddy@99 | 244 Field: "/email", |
| paddy@99 | 245 }) |
| paddy@99 | 246 } |
| paddy@99 | 247 if len(req.Email) > MaxEmailLength { |
| paddy@172 | 248 errors = append(errors, RequestError{ |
| paddy@172 | 249 Slug: RequestErrOverflow, |
| paddy@99 | 250 Field: "/email", |
| paddy@99 | 251 }) |
| paddy@99 | 252 } |
| paddy@99 | 253 re := regexp.MustCompile(".+@.+\\..+") |
| paddy@99 | 254 if !re.Match([]byte(req.Email)) { |
| paddy@172 | 255 errors = append(errors, RequestError{ |
| paddy@172 | 256 Slug: RequestErrInvalidFormat, |
| paddy@99 | 257 Field: "/email", |
| paddy@99 | 258 }) |
| paddy@99 | 259 } |
| paddy@99 | 260 return errors |
| paddy@99 | 261 } |
| paddy@99 | 262 |
| paddy@57 | 263 type profileStore interface { |
| paddy@57 | 264 getProfileByID(id uuid.ID) (Profile, error) |
| paddy@69 | 265 getProfileByLogin(value string) (Profile, error) |
| paddy@57 | 266 saveProfile(profile Profile) error |
| paddy@57 | 267 updateProfile(id uuid.ID, change ProfileChange) error |
| paddy@57 | 268 updateProfiles(ids []uuid.ID, change BulkProfileChange) error |
| paddy@161 | 269 deleteProfile(id uuid.ID) error |
| paddy@44 | 270 |
| paddy@57 | 271 addLogin(login Login) error |
| paddy@172 | 272 getLogin(value string) (Login, error) |
| paddy@69 | 273 removeLogin(value string, profile uuid.ID) error |
| paddy@160 | 274 removeLoginsByProfile(profile uuid.ID) error |
| paddy@69 | 275 recordLoginUse(value string, when time.Time) error |
| paddy@172 | 276 verifyLogin(value, verification string) error |
| paddy@57 | 277 listLogins(profile uuid.ID, num, offset int) ([]Login, error) |
| paddy@38 | 278 } |
| paddy@27 | 279 |
| paddy@57 | 280 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) { |
| paddy@38 | 281 m.profileLock.RLock() |
| paddy@38 | 282 defer m.profileLock.RUnlock() |
| paddy@38 | 283 p, ok := m.profiles[id.String()] |
| paddy@38 | 284 if !ok { |
| paddy@38 | 285 return Profile{}, ErrProfileNotFound |
| paddy@38 | 286 } |
| paddy@38 | 287 return p, nil |
| paddy@27 | 288 } |
| paddy@38 | 289 |
| paddy@69 | 290 func (m *memstore) getProfileByLogin(value string) (Profile, error) { |
| paddy@44 | 291 m.loginLock.RLock() |
| paddy@44 | 292 defer m.loginLock.RUnlock() |
| paddy@69 | 293 login, ok := m.logins[value] |
| paddy@44 | 294 if !ok { |
| paddy@44 | 295 return Profile{}, ErrLoginNotFound |
| paddy@44 | 296 } |
| paddy@44 | 297 m.profileLock.RLock() |
| paddy@44 | 298 defer m.profileLock.RUnlock() |
| paddy@44 | 299 profile, ok := m.profiles[login.ProfileID.String()] |
| paddy@44 | 300 if !ok { |
| paddy@44 | 301 return Profile{}, ErrProfileNotFound |
| paddy@44 | 302 } |
| paddy@44 | 303 return profile, nil |
| paddy@38 | 304 } |
| paddy@38 | 305 |
| paddy@57 | 306 func (m *memstore) saveProfile(profile Profile) error { |
| paddy@38 | 307 m.profileLock.Lock() |
| paddy@38 | 308 defer m.profileLock.Unlock() |
| paddy@38 | 309 _, ok := m.profiles[profile.ID.String()] |
| paddy@38 | 310 if ok { |
| paddy@38 | 311 return ErrProfileAlreadyExists |
| paddy@38 | 312 } |
| paddy@38 | 313 m.profiles[profile.ID.String()] = profile |
| paddy@38 | 314 return nil |
| paddy@38 | 315 } |
| paddy@38 | 316 |
| paddy@57 | 317 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@38 | 318 m.profileLock.Lock() |
| paddy@38 | 319 defer m.profileLock.Unlock() |
| paddy@38 | 320 p, ok := m.profiles[id.String()] |
| paddy@38 | 321 if !ok { |
| paddy@38 | 322 return ErrProfileNotFound |
| paddy@38 | 323 } |
| paddy@38 | 324 p.ApplyChange(change) |
| paddy@38 | 325 m.profiles[id.String()] = p |
| paddy@38 | 326 return nil |
| paddy@38 | 327 } |
| paddy@38 | 328 |
| paddy@57 | 329 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@44 | 330 m.profileLock.Lock() |
| paddy@44 | 331 defer m.profileLock.Unlock() |
| paddy@44 | 332 for id, profile := range m.profiles { |
| paddy@44 | 333 for _, i := range ids { |
| paddy@44 | 334 if id == i.String() { |
| paddy@44 | 335 profile.ApplyBulkChange(change) |
| paddy@44 | 336 m.profiles[id] = profile |
| paddy@44 | 337 break |
| paddy@44 | 338 } |
| paddy@44 | 339 } |
| paddy@44 | 340 } |
| paddy@44 | 341 return nil |
| paddy@44 | 342 } |
| paddy@44 | 343 |
| paddy@161 | 344 func (m *memstore) deleteProfile(id uuid.ID) error { |
| paddy@161 | 345 m.profileLock.Lock() |
| paddy@161 | 346 defer m.profileLock.Unlock() |
| paddy@161 | 347 if _, ok := m.profiles[id.String()]; !ok { |
| paddy@161 | 348 return ErrProfileNotFound |
| paddy@161 | 349 } |
| paddy@161 | 350 delete(m.profiles, id.String()) |
| paddy@161 | 351 return nil |
| paddy@161 | 352 } |
| paddy@161 | 353 |
| paddy@57 | 354 func (m *memstore) addLogin(login Login) error { |
| paddy@44 | 355 m.loginLock.Lock() |
| paddy@44 | 356 defer m.loginLock.Unlock() |
| paddy@69 | 357 _, ok := m.logins[login.Value] |
| paddy@44 | 358 if ok { |
| paddy@44 | 359 return ErrLoginAlreadyExists |
| paddy@44 | 360 } |
| paddy@69 | 361 m.logins[login.Value] = login |
| paddy@69 | 362 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value) |
| paddy@44 | 363 return nil |
| paddy@44 | 364 } |
| paddy@44 | 365 |
| paddy@172 | 366 func (m *memstore) getLogin(value string) (Login, error) { |
| paddy@172 | 367 m.loginLock.RLock() |
| paddy@172 | 368 defer m.loginLock.RUnlock() |
| paddy@172 | 369 l, ok := m.logins[value] |
| paddy@172 | 370 if !ok { |
| paddy@172 | 371 return Login{}, ErrLoginNotFound |
| paddy@172 | 372 } |
| paddy@172 | 373 return l, nil |
| paddy@172 | 374 } |
| paddy@172 | 375 |
| paddy@69 | 376 func (m *memstore) removeLogin(value string, profile uuid.ID) error { |
| paddy@44 | 377 m.loginLock.Lock() |
| paddy@44 | 378 defer m.loginLock.Unlock() |
| paddy@69 | 379 l, ok := m.logins[value] |
| paddy@44 | 380 if !ok { |
| paddy@44 | 381 return ErrLoginNotFound |
| paddy@44 | 382 } |
| paddy@44 | 383 if !l.ProfileID.Equal(profile) { |
| paddy@44 | 384 return ErrLoginNotFound |
| paddy@44 | 385 } |
| paddy@69 | 386 delete(m.logins, value) |
| paddy@44 | 387 pos := -1 |
| paddy@44 | 388 for p, id := range m.profileLoginLookup[profile.String()] { |
| paddy@69 | 389 if id == value { |
| paddy@44 | 390 pos = p |
| paddy@44 | 391 break |
| paddy@44 | 392 } |
| paddy@44 | 393 } |
| paddy@44 | 394 if pos >= 0 { |
| paddy@44 | 395 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...) |
| paddy@44 | 396 } |
| paddy@44 | 397 return nil |
| paddy@44 | 398 } |
| paddy@44 | 399 |
| paddy@160 | 400 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error { |
| paddy@160 | 401 m.loginLock.Lock() |
| paddy@160 | 402 defer m.loginLock.Unlock() |
| paddy@160 | 403 logins, ok := m.profileLoginLookup[profile.String()] |
| paddy@160 | 404 if !ok { |
| paddy@160 | 405 return ErrProfileNotFound |
| paddy@160 | 406 } |
| paddy@160 | 407 delete(m.profileLoginLookup, profile.String()) |
| paddy@160 | 408 for _, login := range logins { |
| paddy@160 | 409 delete(m.logins, login) |
| paddy@160 | 410 } |
| paddy@160 | 411 return nil |
| paddy@160 | 412 } |
| paddy@160 | 413 |
| paddy@69 | 414 func (m *memstore) recordLoginUse(value string, when time.Time) error { |
| paddy@44 | 415 m.loginLock.Lock() |
| paddy@44 | 416 defer m.loginLock.Unlock() |
| paddy@69 | 417 l, ok := m.logins[value] |
| paddy@44 | 418 if !ok { |
| paddy@44 | 419 return ErrLoginNotFound |
| paddy@44 | 420 } |
| paddy@44 | 421 l.LastUsed = when |
| paddy@69 | 422 m.logins[value] = l |
| paddy@44 | 423 return nil |
| paddy@44 | 424 } |
| paddy@44 | 425 |
| paddy@172 | 426 func (m *memstore) verifyLogin(value, verification string) error { |
| paddy@172 | 427 m.loginLock.Lock() |
| paddy@172 | 428 defer m.loginLock.Unlock() |
| paddy@172 | 429 l, ok := m.logins[value] |
| paddy@172 | 430 if !ok { |
| paddy@172 | 431 return ErrLoginNotFound |
| paddy@172 | 432 } |
| paddy@172 | 433 if l.Verification != verification { |
| paddy@172 | 434 return ErrLoginVerificationInvalid |
| paddy@172 | 435 } |
| paddy@172 | 436 l.Verified = true |
| paddy@172 | 437 m.logins[value] = l |
| paddy@172 | 438 return nil |
| paddy@172 | 439 } |
| paddy@172 | 440 |
| paddy@57 | 441 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@44 | 442 m.loginLock.RLock() |
| paddy@44 | 443 defer m.loginLock.RUnlock() |
| paddy@44 | 444 ids, ok := m.profileLoginLookup[profile.String()] |
| paddy@44 | 445 if !ok { |
| paddy@44 | 446 return []Login{}, nil |
| paddy@44 | 447 } |
| paddy@44 | 448 if len(ids) > num+offset { |
| paddy@44 | 449 ids = ids[offset : num+offset] |
| paddy@44 | 450 } else if len(ids) > offset { |
| paddy@44 | 451 ids = ids[offset:] |
| paddy@44 | 452 } else { |
| paddy@44 | 453 return []Login{}, nil |
| paddy@44 | 454 } |
| paddy@44 | 455 logins := []Login{} |
| paddy@44 | 456 for _, id := range ids { |
| paddy@44 | 457 login, ok := m.logins[id] |
| paddy@44 | 458 if !ok { |
| paddy@44 | 459 continue |
| paddy@44 | 460 } |
| paddy@44 | 461 logins = append(logins, login) |
| paddy@44 | 462 } |
| paddy@44 | 463 return logins, nil |
| paddy@44 | 464 } |
| paddy@99 | 465 |
| paddy@160 | 466 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) { |
| paddy@160 | 467 err := context.RemoveLoginsByProfile(profile) |
| paddy@160 | 468 if err != nil { |
| paddy@160 | 469 log.Printf("Error removing logins from profile %s: %+v\n", profile, err) |
| paddy@160 | 470 } |
| paddy@162 | 471 err = context.TerminateSessionsByProfile(profile) |
| paddy@162 | 472 if err != nil { |
| paddy@162 | 473 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err) |
| paddy@162 | 474 } |
| paddy@162 | 475 err = context.RevokeTokensByProfileID(profile) |
| paddy@162 | 476 if err != nil { |
| paddy@162 | 477 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err) |
| paddy@162 | 478 } |
| paddy@163 | 479 err = context.DeleteAuthorizationCodesByProfileID(profile) |
| paddy@163 | 480 if err != nil { |
| paddy@163 | 481 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err) |
| paddy@163 | 482 } |
| paddy@164 | 483 clients, err := context.ListClientsByOwner(profile, -1, 0) |
| paddy@164 | 484 if err != nil { |
| paddy@164 | 485 log.Printf("Error listing clients by profile %s: %+v\n", profile, err) |
| paddy@164 | 486 } |
| paddy@164 | 487 err = context.DeleteClientsByOwner(profile) |
| paddy@164 | 488 if err != nil { |
| paddy@164 | 489 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err) |
| paddy@164 | 490 } |
| paddy@164 | 491 for _, client := range clients { |
| paddy@164 | 492 cleanUpAfterClientDeletion(client.ID, context) |
| paddy@164 | 493 } |
| paddy@160 | 494 } |
| paddy@160 | 495 |
| paddy@105 | 496 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval. |
| paddy@105 | 497 func RegisterProfileHandlers(r *mux.Router, context Context) { |
| paddy@165 | 498 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS") |
| paddy@166 | 499 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS") |
| paddy@165 | 500 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS") |
| paddy@165 | 501 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS") |
| paddy@128 | 502 // BUG(paddy): We need to implement a handler that will add a login to a profile. |
| paddy@128 | 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? |
| paddy@128 | 504 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile. |
| paddy@172 | 505 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS") |
| paddy@172 | 506 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS") |
| paddy@105 | 507 } |
| paddy@105 | 508 |
| paddy@166 | 509 // GetProfileHandler is an HTTP handler for retrieving a profile. |
| paddy@166 | 510 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 511 errors := []RequestError{} |
| paddy@166 | 512 authz := r.Header.Get("Authorization") |
| paddy@166 | 513 if !strings.HasPrefix(authz, "Bearer ") { |
| paddy@172 | 514 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 515 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@166 | 516 return |
| paddy@166 | 517 } |
| paddy@166 | 518 authz = strings.TrimPrefix(authz, "Bearer ") |
| paddy@166 | 519 vars := mux.Vars(r) |
| paddy@166 | 520 if vars["id"] == "" { |
| paddy@172 | 521 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 522 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@166 | 523 return |
| paddy@166 | 524 } |
| paddy@166 | 525 id, err := uuid.Parse(vars["id"]) |
| paddy@166 | 526 if err != nil { |
| paddy@172 | 527 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) |
| paddy@172 | 528 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@166 | 529 return |
| paddy@166 | 530 } |
| paddy@166 | 531 token, err := context.GetToken(authz, false) |
| paddy@166 | 532 if err != nil || token.Revoked { |
| paddy@166 | 533 if err == ErrTokenNotFound || token.Revoked { |
| paddy@172 | 534 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 535 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@166 | 536 return |
| paddy@166 | 537 } else { |
| paddy@166 | 538 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@166 | 539 return |
| paddy@166 | 540 } |
| paddy@166 | 541 } |
| paddy@166 | 542 if !id.Equal(token.ProfileID) { |
| paddy@172 | 543 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 544 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@166 | 545 return |
| paddy@166 | 546 } |
| paddy@166 | 547 profile, err := context.GetProfileByID(id) |
| paddy@166 | 548 if err != nil { |
| paddy@166 | 549 if err == ErrProfileNotFound { |
| paddy@172 | 550 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) |
| paddy@172 | 551 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@166 | 552 return |
| paddy@166 | 553 } |
| paddy@166 | 554 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@166 | 555 return |
| paddy@166 | 556 } |
| paddy@172 | 557 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) |
| paddy@166 | 558 return |
| paddy@166 | 559 } |
| paddy@166 | 560 |
| paddy@99 | 561 // CreateProfileHandler is an HTTP handler for registering new profiles. |
| paddy@99 | 562 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@99 | 563 scheme, ok := passphraseSchemes[CurPassphraseScheme] |
| paddy@99 | 564 if !ok { |
| paddy@149 | 565 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme) |
| paddy@105 | 566 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 567 return |
| paddy@99 | 568 } |
| paddy@99 | 569 var req newProfileRequest |
| paddy@172 | 570 errors := []RequestError{} |
| paddy@99 | 571 decoder := json.NewDecoder(r.Body) |
| paddy@99 | 572 err := decoder.Decode(&req) |
| paddy@99 | 573 if err != nil { |
| paddy@105 | 574 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@99 | 575 return |
| paddy@99 | 576 } |
| paddy@99 | 577 errors = append(errors, validateNewProfileRequest(&req)...) |
| paddy@99 | 578 if len(errors) > 0 { |
| paddy@172 | 579 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@99 | 580 return |
| paddy@99 | 581 } |
| paddy@99 | 582 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations) |
| paddy@99 | 583 if err != nil { |
| paddy@149 | 584 log.Printf("Error creating encoded passphrase: %#+v\n", err) |
| paddy@105 | 585 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 586 return |
| paddy@99 | 587 } |
| paddy@99 | 588 profile := Profile{ |
| paddy@99 | 589 ID: uuid.NewID(), |
| paddy@99 | 590 Name: req.Name, |
| paddy@99 | 591 Passphrase: string(passphrase), |
| paddy@99 | 592 Iterations: context.config.iterations, |
| paddy@99 | 593 Salt: string(salt), |
| paddy@99 | 594 PassphraseScheme: CurPassphraseScheme, |
| paddy@99 | 595 Created: time.Now(), |
| paddy@99 | 596 LastSeen: time.Now(), |
| paddy@99 | 597 } |
| paddy@99 | 598 err = context.SaveProfile(profile) |
| paddy@99 | 599 if err != nil { |
| paddy@105 | 600 if err == ErrProfileAlreadyExists { |
| paddy@172 | 601 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}}) |
| paddy@105 | 602 return |
| paddy@105 | 603 } |
| paddy@149 | 604 log.Printf("Error saving profile: %#+v\n", err) |
| paddy@105 | 605 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 606 return |
| paddy@99 | 607 } |
| paddy@99 | 608 logins := []Login{} |
| paddy@99 | 609 login := Login{ |
| paddy@172 | 610 Type: "email", |
| paddy@172 | 611 Value: req.Email, |
| paddy@172 | 612 Created: profile.Created, |
| paddy@172 | 613 LastUsed: profile.Created, |
| paddy@172 | 614 ProfileID: profile.ID, |
| paddy@172 | 615 Verification: uuid.NewID().String(), |
| paddy@99 | 616 } |
| paddy@99 | 617 err = context.AddLogin(login) |
| paddy@99 | 618 if err != nil { |
| paddy@105 | 619 if err == ErrLoginAlreadyExists { |
| paddy@172 | 620 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}}) |
| paddy@105 | 621 return |
| paddy@105 | 622 } |
| paddy@149 | 623 log.Printf("Error adding login: %#+v\n", err) |
| paddy@105 | 624 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 625 return |
| paddy@99 | 626 } |
| paddy@99 | 627 logins = append(logins, login) |
| paddy@172 | 628 resp := Response{ |
| paddy@105 | 629 Logins: logins, |
| paddy@105 | 630 Profiles: []Profile{profile}, |
| paddy@105 | 631 } |
| paddy@105 | 632 encode(w, r, http.StatusCreated, resp) |
| paddy@172 | 633 go context.SendLoginVerification(login) |
| paddy@99 | 634 } |
| paddy@145 | 635 |
| paddy@145 | 636 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 637 errors := []RequestError{} |
| paddy@145 | 638 vars := mux.Vars(r) |
| paddy@145 | 639 if vars["id"] == "" { |
| paddy@172 | 640 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 641 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@145 | 642 return |
| paddy@145 | 643 } |
| paddy@145 | 644 id, err := uuid.Parse(vars["id"]) |
| paddy@145 | 645 if err != nil { |
| paddy@172 | 646 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 647 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@145 | 648 return |
| paddy@145 | 649 } |
| paddy@145 | 650 username, password, ok := r.BasicAuth() |
| paddy@145 | 651 if !ok { |
| paddy@172 | 652 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 653 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@145 | 654 return |
| paddy@145 | 655 } |
| paddy@145 | 656 profile, err := authenticate(username, password, context) |
| paddy@145 | 657 if err != nil { |
| paddy@145 | 658 if isAuthError(err) { |
| paddy@172 | 659 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 660 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@145 | 661 } else { |
| paddy@172 | 662 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 663 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@145 | 664 } |
| paddy@145 | 665 return |
| paddy@145 | 666 } |
| paddy@145 | 667 if !profile.ID.Equal(id) { |
| paddy@172 | 668 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 669 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@145 | 670 return |
| paddy@145 | 671 } |
| paddy@145 | 672 var req ProfileChange |
| paddy@145 | 673 decoder := json.NewDecoder(r.Body) |
| paddy@145 | 674 err = decoder.Decode(&req) |
| paddy@145 | 675 if err != nil { |
| paddy@149 | 676 log.Printf("Error decoding request: %#+v\n", err) |
| paddy@145 | 677 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@145 | 678 return |
| paddy@145 | 679 } |
| paddy@145 | 680 req.Iterations = nil |
| paddy@145 | 681 req.Salt = nil |
| paddy@145 | 682 req.PassphraseScheme = nil |
| paddy@145 | 683 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised |
| paddy@145 | 684 req.LockedUntil = nil |
| paddy@145 | 685 req.LastSeen = nil |
| paddy@145 | 686 if req.Passphrase != nil { |
| paddy@145 | 687 if len(*req.Passphrase) < MinPassphraseLength { |
| paddy@172 | 688 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"}) |
| paddy@172 | 689 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@145 | 690 return |
| paddy@145 | 691 } |
| paddy@145 | 692 if len(*req.Passphrase) > MaxPassphraseLength { |
| paddy@172 | 693 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"}) |
| paddy@172 | 694 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@145 | 695 return |
| paddy@145 | 696 } |
| paddy@145 | 697 iterations := context.config.iterations |
| paddy@145 | 698 scheme, ok := passphraseSchemes[CurPassphraseScheme] |
| paddy@145 | 699 if !ok { |
| paddy@145 | 700 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@145 | 701 return |
| paddy@145 | 702 } |
| paddy@145 | 703 curScheme := CurPassphraseScheme |
| paddy@145 | 704 req.PassphraseScheme = &curScheme |
| paddy@145 | 705 passphrase, salt, err := scheme.create(*req.Passphrase, iterations) |
| paddy@145 | 706 if err != nil { |
| paddy@145 | 707 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@145 | 708 return |
| paddy@145 | 709 } |
| paddy@145 | 710 req.Passphrase = &passphrase |
| paddy@145 | 711 req.Salt = &salt |
| paddy@145 | 712 req.Iterations = &iterations |
| paddy@145 | 713 } |
| paddy@145 | 714 if req.PassphraseReset != nil { |
| paddy@145 | 715 now := time.Now() |
| paddy@145 | 716 req.PassphraseResetCreated = &now |
| paddy@145 | 717 } |
| paddy@145 | 718 err = req.Validate() |
| paddy@145 | 719 if err != nil { |
| paddy@145 | 720 var status int |
| paddy@172 | 721 var resp Response |
| paddy@145 | 722 switch err { |
| paddy@145 | 723 case ErrEmptyChange: |
| paddy@145 | 724 resp.Profiles = []Profile{profile} |
| paddy@145 | 725 status = http.StatusOK |
| paddy@145 | 726 default: |
| paddy@172 | 727 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@145 | 728 resp.Errors = errors |
| paddy@145 | 729 status = http.StatusInternalServerError |
| paddy@145 | 730 } |
| paddy@145 | 731 encode(w, r, status, resp) |
| paddy@145 | 732 return |
| paddy@145 | 733 } |
| paddy@145 | 734 err = context.UpdateProfile(id, req) |
| paddy@145 | 735 if err != nil { |
| paddy@145 | 736 if err == ErrProfileNotFound { |
| paddy@172 | 737 errors = append(errors, RequestError{Slug: RequestErrNotFound}) |
| paddy@172 | 738 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@145 | 739 return |
| paddy@145 | 740 } |
| paddy@145 | 741 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@145 | 742 return |
| paddy@145 | 743 } |
| paddy@145 | 744 profile.ApplyChange(req) |
| paddy@172 | 745 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) |
| paddy@145 | 746 return |
| paddy@145 | 747 } |
| paddy@160 | 748 |
| paddy@160 | 749 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 750 errors := []RequestError{} |
| paddy@160 | 751 vars := mux.Vars(r) |
| paddy@160 | 752 if vars["id"] == "" { |
| paddy@172 | 753 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 754 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@160 | 755 return |
| paddy@160 | 756 } |
| paddy@160 | 757 id, err := uuid.Parse(vars["id"]) |
| paddy@160 | 758 if err != nil { |
| paddy@172 | 759 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 760 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@160 | 761 return |
| paddy@160 | 762 } |
| paddy@160 | 763 username, password, ok := r.BasicAuth() |
| paddy@160 | 764 if !ok { |
| paddy@172 | 765 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 766 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@160 | 767 return |
| paddy@160 | 768 } |
| paddy@160 | 769 profile, err := authenticate(username, password, context) |
| paddy@160 | 770 if err != nil { |
| paddy@160 | 771 if isAuthError(err) { |
| paddy@172 | 772 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 773 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@160 | 774 } else { |
| paddy@172 | 775 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 776 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@160 | 777 } |
| paddy@160 | 778 return |
| paddy@160 | 779 } |
| paddy@160 | 780 if !profile.ID.Equal(id) { |
| paddy@172 | 781 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 782 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@160 | 783 return |
| paddy@160 | 784 } |
| paddy@161 | 785 err = context.DeleteProfile(id) |
| paddy@160 | 786 if err != nil { |
| paddy@160 | 787 if err == ErrProfileNotFound { |
| paddy@172 | 788 errors = append(errors, RequestError{Slug: RequestErrNotFound}) |
| paddy@172 | 789 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@160 | 790 return |
| paddy@160 | 791 } |
| paddy@160 | 792 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@160 | 793 return |
| paddy@160 | 794 } |
| paddy@172 | 795 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) |
| paddy@160 | 796 go cleanUpAfterProfileDeletion(profile.ID, context) |
| paddy@160 | 797 } |
| paddy@172 | 798 |
| paddy@172 | 799 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 800 var errors []RequestError |
| paddy@172 | 801 vars := mux.Vars(r) |
| paddy@172 | 802 if vars["login"] == "" { |
| paddy@172 | 803 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"}) |
| paddy@172 | 804 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 805 return |
| paddy@172 | 806 } |
| paddy@172 | 807 login, err := context.GetLogin(vars["login"]) |
| paddy@172 | 808 if err != nil { |
| paddy@172 | 809 if err == ErrLoginNotFound { |
| paddy@172 | 810 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) |
| paddy@172 | 811 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@172 | 812 return |
| paddy@172 | 813 } |
| paddy@172 | 814 log.Printf("Error retrieving login: %#+v\n", err) |
| paddy@172 | 815 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@172 | 816 return |
| paddy@172 | 817 } |
| paddy@172 | 818 encode(w, r, http.StatusOK, Response{Logins: []Login{login}}) |
| paddy@172 | 819 } |
| paddy@172 | 820 |
| paddy@172 | 821 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 822 var errors []RequestError |
| paddy@172 | 823 vars := mux.Vars(r) |
| paddy@172 | 824 if vars["login"] == "" { |
| paddy@172 | 825 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"}) |
| paddy@172 | 826 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 827 return |
| paddy@172 | 828 } |
| paddy@172 | 829 var req LoginChange |
| paddy@172 | 830 decoder := json.NewDecoder(r.Body) |
| paddy@172 | 831 err := decoder.Decode(&req) |
| paddy@172 | 832 if err != nil { |
| paddy@172 | 833 log.Printf("Error decoding request: %#+v\n", err) |
| paddy@172 | 834 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@172 | 835 return |
| paddy@172 | 836 } |
| paddy@172 | 837 login, err := context.GetLogin(vars["login"]) |
| paddy@172 | 838 if err != nil { |
| paddy@172 | 839 if err == ErrLoginNotFound { |
| paddy@172 | 840 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) |
| paddy@172 | 841 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@172 | 842 return |
| paddy@172 | 843 } |
| paddy@172 | 844 log.Printf("Error retrieving login: %#+v\n", err) |
| paddy@172 | 845 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@172 | 846 return |
| paddy@172 | 847 } |
| paddy@172 | 848 if req.Verification != nil { |
| paddy@172 | 849 err = context.VerifyLogin(vars["login"], *req.Verification) |
| paddy@172 | 850 if err != nil { |
| paddy@172 | 851 if err == ErrLoginNotFound { |
| paddy@172 | 852 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) |
| paddy@172 | 853 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@172 | 854 return |
| paddy@172 | 855 } else if err == ErrLoginVerificationInvalid { |
| paddy@172 | 856 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"}) |
| paddy@172 | 857 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 858 return |
| paddy@172 | 859 } |
| paddy@172 | 860 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err) |
| paddy@172 | 861 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@172 | 862 return |
| paddy@172 | 863 } |
| paddy@172 | 864 login.Verified = true |
| paddy@172 | 865 } else if req.ResendVerification != nil { |
| paddy@172 | 866 if !*req.ResendVerification { |
| paddy@172 | 867 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"}) |
| paddy@172 | 868 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 869 return |
| paddy@172 | 870 } |
| paddy@172 | 871 context.SendLoginVerification(login) |
| paddy@172 | 872 } else { |
| paddy@172 | 873 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"}) |
| paddy@172 | 874 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 875 return |
| paddy@172 | 876 } |
| paddy@172 | 877 encode(w, r, http.StatusOK, Response{Logins: []Login{login}}) |
| paddy@172 | 878 } |