auth

Paddy 2015-07-15 Parent:5d52b9d83184 Child:7bba108d2d9a

178:0a2c3d677161 Go to Latest

auth/profile.go

Update to use a generic event emitter. Rather can creating a purpose-built event emitter for each and every event we need to emit (I'm looking at you, login verification event) which is _downright silly_, we're now using a generic event publisher that's based on saying "HEY A MODEL UPDATED". This means we need to change all our setup code in authd to use events.NewNSQPublisher or events.NewStdoutPublisher instead of our homegrown solutions. Which also means updating our config to take an events.Publisher instead of our LoginVerificationNotifier (blergh). Our Context also now uses an events.Publisher instead of a LoginVerificationNotifier. Party all around! We also replaced our SendLoginVerification helper method on Context with a SendModelEvent helper method on Context, which is just a light wrapper around events.PublishModelEvent. Of course, all this means we need to update our email_verification listener to listen to the correct channel (based on the model we want updates about) and filter down to a Created action or our new custom action for "the customer wants their verification resent", which I'm OK making a special case and not generic, because c'mon. But we had a subtle change to all our constants, some of which are unofficial constants now. I'm unsure how I feel about this. We also updated our email_verification listener so that we're unmarshalling to a custom loginEvent, which is just an events.Event that overwrites the Data property to be an auth.Login instance. This is to make sure we don't need to wrangle a map[string]interface{}, which is no fun. I'm also OK with special-casing like this, because it's 1) a tiny amount of code, 2) properly utilising composition, and 3) the only way I can think of to cleanly accomplish what I want. I also added a note about GetLogin's deficient handling of logins, namely that it doesn't recognise admins and return Verification codes to them, which would be a useful property for internal tools to take advantage of. Ah well. I updated the Profile and Login implementations so they're now event.Model instances, mainly by just exporting some strings from them through getters that will let us automatically build an Event from them. This lets us use the PublishModelEvent helper. I updated our CreateProfileHandler to properly mangle the login Verification property, and to fire off the ActionCreated events for the new Login and the new Profile. I updated our GetLoginHandler and UpdateLoginHandler to properly mangle the loginVerification property. God that's annoying. :-/ You'll note I didn't start publishing the events.ActionUpdated or events.ActionDeleted events for Profiles or Logins yet, and didn't bother publishing any events for literally any other type. That's because I'm a lazy piece of crap and will end up publishing them when I absolutely have to. Part of that is because if a channel isn't created/being read for a topic, the messages will just stack up in NSQ, and I don't want that. But mostly I'm lazy. Finally, I got to delete the entire profile_verification.go file, because we're no longer special-casing that. Hooray!

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