auth

Paddy 2015-03-07 Parent:23c1a07c8a61 Child:e660a38fa936

141:a8e6122bfc1a Go to Latest

auth/profile.go

Require authentication to update Clients. Require the Client's owner to supply basic authentication when updating a client.

History
paddy@27 1 package auth
paddy@27 2
paddy@27 3 import (
paddy@99 4 "encoding/json"
paddy@38 5 "errors"
paddy@99 6 "net/http"
paddy@99 7 "regexp"
paddy@99 8 "strings"
paddy@27 9 "time"
paddy@27 10
paddy@107 11 "code.secondbit.org/uuid.hg"
paddy@99 12 "github.com/extemporalgenome/slug"
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 // MaxUsernameLength is the maximum length, in bytes, of a username, exclusive.
paddy@99 26 MaxUsernameLength = 16
paddy@99 27 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
paddy@99 28 MaxEmailLength = 64
paddy@48 29 )
paddy@48 30
paddy@38 31 var (
paddy@57 32 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
paddy@57 33 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
paddy@57 34 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
paddy@57 35 // the same ID already exists in the profileStore.
paddy@57 36 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
paddy@57 37 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
paddy@57 38 ErrProfileNotFound = errors.New("profile not found in profileStore")
paddy@57 39 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
paddy@57 40 // Type and Value already exists in the profileStore.
paddy@57 41 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
paddy@57 42 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
paddy@57 43 ErrLoginNotFound = errors.New("login not found in profileStore")
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@57 70 // including their authentication information, but not
paddy@57 71 // including their username or email.
paddy@27 72 type Profile struct {
paddy@105 73 ID uuid.ID `json:"id,omitempty"`
paddy@105 74 Name string `json:"name,omitempty"`
paddy@105 75 Passphrase string `json:"-"`
paddy@105 76 Iterations int `json:"-"`
paddy@105 77 Salt string `json:"-"`
paddy@105 78 PassphraseScheme int `json:"-"`
paddy@105 79 Compromised bool `json:"-"`
paddy@105 80 LockedUntil time.Time `json:"-"`
paddy@105 81 PassphraseReset string `json:"-"`
paddy@105 82 PassphraseResetCreated time.Time `json:"-"`
paddy@105 83 Created time.Time `json:"created,omitempty"`
paddy@105 84 LastSeen time.Time `json:"last_seen,omitempty"`
paddy@38 85 }
paddy@38 86
paddy@57 87 // ApplyChange applies the properties of the passed ProfileChange
paddy@57 88 // to the Profile it is called on.
paddy@38 89 func (p *Profile) ApplyChange(change ProfileChange) {
paddy@38 90 if change.Name != nil {
paddy@38 91 p.Name = *change.Name
paddy@38 92 }
paddy@38 93 if change.Passphrase != nil {
paddy@38 94 p.Passphrase = *change.Passphrase
paddy@38 95 }
paddy@38 96 if change.Iterations != nil {
paddy@38 97 p.Iterations = *change.Iterations
paddy@38 98 }
paddy@38 99 if change.Salt != nil {
paddy@38 100 p.Salt = *change.Salt
paddy@38 101 }
paddy@38 102 if change.PassphraseScheme != nil {
paddy@38 103 p.PassphraseScheme = *change.PassphraseScheme
paddy@38 104 }
paddy@38 105 if change.Compromised != nil {
paddy@38 106 p.Compromised = *change.Compromised
paddy@38 107 }
paddy@38 108 if change.LockedUntil != nil {
paddy@38 109 p.LockedUntil = *change.LockedUntil
paddy@38 110 }
paddy@38 111 if change.PassphraseReset != nil {
paddy@38 112 p.PassphraseReset = *change.PassphraseReset
paddy@38 113 }
paddy@38 114 if change.PassphraseResetCreated != nil {
paddy@38 115 p.PassphraseResetCreated = *change.PassphraseResetCreated
paddy@38 116 }
paddy@38 117 if change.LastSeen != nil {
paddy@38 118 p.LastSeen = *change.LastSeen
paddy@38 119 }
paddy@38 120 }
paddy@38 121
paddy@57 122 // ApplyBulkChange applies the properties of the passed BulkProfileChange
paddy@57 123 // to the Profile it is called on.
paddy@44 124 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
paddy@44 125 if change.Compromised != nil {
paddy@44 126 p.Compromised = *change.Compromised
paddy@44 127 }
paddy@44 128 }
paddy@44 129
paddy@57 130 // ProfileChange represents a single atomic change to a Profile's mutable data.
paddy@38 131 type ProfileChange struct {
paddy@38 132 Name *string
paddy@38 133 Passphrase *string
paddy@69 134 Iterations *int
paddy@38 135 Salt *string
paddy@38 136 PassphraseScheme *int
paddy@38 137 Compromised *bool
paddy@38 138 LockedUntil *time.Time
paddy@38 139 PassphraseReset *string
paddy@38 140 PassphraseResetCreated *time.Time
paddy@38 141 LastSeen *time.Time
paddy@38 142 }
paddy@38 143
paddy@57 144 // Validate checks the ProfileChange it is called on
paddy@57 145 // and asserts its internal validity, or lack thereof.
paddy@57 146 // A descriptive error will be returned in the case of
paddy@57 147 // an invalid change.
paddy@38 148 func (c ProfileChange) Validate() error {
paddy@48 149 if 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@48 150 return ErrEmptyChange
paddy@48 151 }
paddy@48 152 if c.PassphraseScheme != nil && c.Passphrase == nil {
paddy@48 153 return ErrMissingPassphrase
paddy@48 154 }
paddy@48 155 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
paddy@48 156 return ErrMissingPassphraseResetCreated
paddy@48 157 }
paddy@48 158 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
paddy@48 159 return ErrMissingPassphraseReset
paddy@48 160 }
paddy@48 161 if c.Salt != nil && c.Passphrase == nil {
paddy@48 162 return ErrMissingPassphrase
paddy@48 163 }
paddy@48 164 if c.Iterations != nil && c.Passphrase == nil {
paddy@48 165 return ErrMissingPassphrase
paddy@48 166 }
paddy@48 167 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength {
paddy@48 168 return ErrPassphraseTooShort
paddy@48 169 }
paddy@48 170 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength {
paddy@48 171 return ErrPassphraseTooLong
paddy@48 172 }
paddy@38 173 return nil
paddy@27 174 }
paddy@27 175
paddy@57 176 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
paddy@57 177 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
paddy@57 178 // ProfileChange values across many Profiles all at once.
paddy@44 179 type BulkProfileChange struct {
paddy@44 180 Compromised *bool
paddy@44 181 }
paddy@44 182
paddy@57 183 // Validate checks the BulkProfileChange it is called on
paddy@57 184 // and asserts its internal validity, or lack thereof.
paddy@57 185 // A descriptive error will be returned in the case of an
paddy@57 186 // invalid change.
paddy@44 187 func (b BulkProfileChange) Validate() error {
paddy@48 188 if b.Compromised == nil {
paddy@48 189 return ErrEmptyChange
paddy@48 190 }
paddy@44 191 return nil
paddy@44 192 }
paddy@44 193
paddy@57 194 // Login represents a single human-friendly identifier for
paddy@57 195 // a given Profile that can be used to log into that Profile.
paddy@57 196 // Each Profile may only have one Login for each Type.
paddy@27 197 type Login struct {
paddy@105 198 Type string `json:"type,omitempty"`
paddy@105 199 Value string `json:"value,omitempty"`
paddy@105 200 ProfileID uuid.ID `json:"profile_id,omitempty"`
paddy@105 201 Created time.Time `json:"created,omitempty"`
paddy@105 202 LastUsed time.Time `json:"last_used,omitempty"`
paddy@27 203 }
paddy@27 204
paddy@99 205 type newProfileRequest struct {
paddy@99 206 Username string `json:"username"`
paddy@99 207 Email string `json:"email"`
paddy@99 208 Passphrase string `json:"passphrase"`
paddy@99 209 Name string `json:"name"`
paddy@99 210 }
paddy@99 211
paddy@99 212 func validateNewProfileRequest(req *newProfileRequest) []requestError {
paddy@99 213 errors := []requestError{}
paddy@99 214 req.Name = strings.TrimSpace(req.Name)
paddy@99 215 req.Email = strings.TrimSpace(req.Email)
paddy@99 216 req.Username = slug.SlugAscii(strings.TrimSpace(req.Username))
paddy@99 217 if len(req.Passphrase) < MinPassphraseLength {
paddy@99 218 errors = append(errors, requestError{
paddy@99 219 Slug: requestErrInsufficient,
paddy@99 220 Field: "/passphrase",
paddy@99 221 })
paddy@99 222 }
paddy@99 223 if len(req.Passphrase) > MaxPassphraseLength {
paddy@99 224 errors = append(errors, requestError{
paddy@99 225 Slug: requestErrOverflow,
paddy@99 226 Field: "/passphrase",
paddy@99 227 })
paddy@99 228 }
paddy@99 229 if len(req.Name) > MaxNameLength {
paddy@99 230 errors = append(errors, requestError{
paddy@99 231 Slug: requestErrOverflow,
paddy@99 232 Field: "/name",
paddy@99 233 })
paddy@99 234 }
paddy@99 235 if len(req.Username) > MaxUsernameLength {
paddy@99 236 errors = append(errors, requestError{
paddy@99 237 Slug: requestErrOverflow,
paddy@99 238 Field: "/username",
paddy@99 239 })
paddy@99 240 }
paddy@99 241 if req.Email == "" {
paddy@99 242 errors = append(errors, requestError{
paddy@99 243 Slug: requestErrMissing,
paddy@99 244 Field: "/email",
paddy@99 245 })
paddy@99 246 }
paddy@99 247 if len(req.Email) > MaxEmailLength {
paddy@99 248 errors = append(errors, requestError{
paddy@99 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@99 255 errors = append(errors, requestError{
paddy@105 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@57 269 deleteProfile(id uuid.ID) error
paddy@44 270
paddy@57 271 addLogin(login Login) error
paddy@69 272 removeLogin(value string, profile uuid.ID) error
paddy@69 273 recordLoginUse(value string, when time.Time) error
paddy@57 274 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
paddy@38 275 }
paddy@27 276
paddy@57 277 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
paddy@38 278 m.profileLock.RLock()
paddy@38 279 defer m.profileLock.RUnlock()
paddy@38 280 p, ok := m.profiles[id.String()]
paddy@38 281 if !ok {
paddy@38 282 return Profile{}, ErrProfileNotFound
paddy@38 283 }
paddy@38 284 return p, nil
paddy@27 285 }
paddy@38 286
paddy@69 287 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
paddy@44 288 m.loginLock.RLock()
paddy@44 289 defer m.loginLock.RUnlock()
paddy@69 290 login, ok := m.logins[value]
paddy@44 291 if !ok {
paddy@44 292 return Profile{}, ErrLoginNotFound
paddy@44 293 }
paddy@44 294 m.profileLock.RLock()
paddy@44 295 defer m.profileLock.RUnlock()
paddy@44 296 profile, ok := m.profiles[login.ProfileID.String()]
paddy@44 297 if !ok {
paddy@44 298 return Profile{}, ErrProfileNotFound
paddy@44 299 }
paddy@44 300 return profile, nil
paddy@38 301 }
paddy@38 302
paddy@57 303 func (m *memstore) saveProfile(profile Profile) error {
paddy@38 304 m.profileLock.Lock()
paddy@38 305 defer m.profileLock.Unlock()
paddy@38 306 _, ok := m.profiles[profile.ID.String()]
paddy@38 307 if ok {
paddy@38 308 return ErrProfileAlreadyExists
paddy@38 309 }
paddy@38 310 m.profiles[profile.ID.String()] = profile
paddy@38 311 return nil
paddy@38 312 }
paddy@38 313
paddy@57 314 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
paddy@38 315 m.profileLock.Lock()
paddy@38 316 defer m.profileLock.Unlock()
paddy@38 317 p, ok := m.profiles[id.String()]
paddy@38 318 if !ok {
paddy@38 319 return ErrProfileNotFound
paddy@38 320 }
paddy@38 321 p.ApplyChange(change)
paddy@38 322 m.profiles[id.String()] = p
paddy@38 323 return nil
paddy@38 324 }
paddy@38 325
paddy@57 326 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
paddy@44 327 m.profileLock.Lock()
paddy@44 328 defer m.profileLock.Unlock()
paddy@44 329 for id, profile := range m.profiles {
paddy@44 330 for _, i := range ids {
paddy@44 331 if id == i.String() {
paddy@44 332 profile.ApplyBulkChange(change)
paddy@44 333 m.profiles[id] = profile
paddy@44 334 break
paddy@44 335 }
paddy@44 336 }
paddy@44 337 }
paddy@44 338 return nil
paddy@44 339 }
paddy@44 340
paddy@57 341 func (m *memstore) deleteProfile(id uuid.ID) error {
paddy@38 342 m.profileLock.Lock()
paddy@38 343 defer m.profileLock.Unlock()
paddy@38 344 _, ok := m.profiles[id.String()]
paddy@38 345 if !ok {
paddy@38 346 return ErrProfileNotFound
paddy@38 347 }
paddy@38 348 delete(m.profiles, id.String())
paddy@38 349 return nil
paddy@38 350 }
paddy@40 351
paddy@57 352 func (m *memstore) addLogin(login Login) error {
paddy@44 353 m.loginLock.Lock()
paddy@44 354 defer m.loginLock.Unlock()
paddy@69 355 _, ok := m.logins[login.Value]
paddy@44 356 if ok {
paddy@44 357 return ErrLoginAlreadyExists
paddy@44 358 }
paddy@69 359 m.logins[login.Value] = login
paddy@69 360 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
paddy@44 361 return nil
paddy@44 362 }
paddy@44 363
paddy@69 364 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
paddy@44 365 m.loginLock.Lock()
paddy@44 366 defer m.loginLock.Unlock()
paddy@69 367 l, ok := m.logins[value]
paddy@44 368 if !ok {
paddy@44 369 return ErrLoginNotFound
paddy@44 370 }
paddy@44 371 if !l.ProfileID.Equal(profile) {
paddy@44 372 return ErrLoginNotFound
paddy@44 373 }
paddy@69 374 delete(m.logins, value)
paddy@44 375 pos := -1
paddy@44 376 for p, id := range m.profileLoginLookup[profile.String()] {
paddy@69 377 if id == value {
paddy@44 378 pos = p
paddy@44 379 break
paddy@44 380 }
paddy@44 381 }
paddy@44 382 if pos >= 0 {
paddy@44 383 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
paddy@44 384 }
paddy@44 385 return nil
paddy@44 386 }
paddy@44 387
paddy@69 388 func (m *memstore) recordLoginUse(value string, when time.Time) error {
paddy@44 389 m.loginLock.Lock()
paddy@44 390 defer m.loginLock.Unlock()
paddy@69 391 l, ok := m.logins[value]
paddy@44 392 if !ok {
paddy@44 393 return ErrLoginNotFound
paddy@44 394 }
paddy@44 395 l.LastUsed = when
paddy@69 396 m.logins[value] = l
paddy@44 397 return nil
paddy@44 398 }
paddy@44 399
paddy@57 400 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
paddy@44 401 m.loginLock.RLock()
paddy@44 402 defer m.loginLock.RUnlock()
paddy@44 403 ids, ok := m.profileLoginLookup[profile.String()]
paddy@44 404 if !ok {
paddy@44 405 return []Login{}, nil
paddy@44 406 }
paddy@44 407 if len(ids) > num+offset {
paddy@44 408 ids = ids[offset : num+offset]
paddy@44 409 } else if len(ids) > offset {
paddy@44 410 ids = ids[offset:]
paddy@44 411 } else {
paddy@44 412 return []Login{}, nil
paddy@44 413 }
paddy@44 414 logins := []Login{}
paddy@44 415 for _, id := range ids {
paddy@44 416 login, ok := m.logins[id]
paddy@44 417 if !ok {
paddy@44 418 continue
paddy@44 419 }
paddy@44 420 logins = append(logins, login)
paddy@44 421 }
paddy@44 422 return logins, nil
paddy@44 423 }
paddy@99 424
paddy@105 425 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
paddy@105 426 func RegisterProfileHandlers(r *mux.Router, context Context) {
paddy@105 427 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST")
paddy@128 428 // BUG(paddy): We need to implement a handler that will return information about a profile or set of profiles.
paddy@128 429 // BUG(paddy): We need to implement a handler that will update a profile.
paddy@128 430 // BUG(paddy): We need to implement a handler that will delete a profile. What happens to clients/tokens/grants/sessions when a profile is deleted?
paddy@128 431 // BUG(paddy): We need to implement a handler that will add a login to a profile.
paddy@128 432 // 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 433 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
paddy@105 434 }
paddy@105 435
paddy@99 436 // CreateProfileHandler is an HTTP handler for registering new profiles.
paddy@99 437 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@99 438 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@99 439 if !ok {
paddy@105 440 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 441 return
paddy@99 442 }
paddy@99 443 var req newProfileRequest
paddy@99 444 errors := []requestError{}
paddy@99 445 decoder := json.NewDecoder(r.Body)
paddy@99 446 err := decoder.Decode(&req)
paddy@99 447 if err != nil {
paddy@105 448 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@99 449 return
paddy@99 450 }
paddy@99 451 errors = append(errors, validateNewProfileRequest(&req)...)
paddy@99 452 if len(errors) > 0 {
paddy@105 453 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@99 454 return
paddy@99 455 }
paddy@99 456 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
paddy@99 457 if err != nil {
paddy@105 458 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 459 return
paddy@99 460 }
paddy@99 461 profile := Profile{
paddy@99 462 ID: uuid.NewID(),
paddy@99 463 Name: req.Name,
paddy@99 464 Passphrase: string(passphrase),
paddy@99 465 Iterations: context.config.iterations,
paddy@99 466 Salt: string(salt),
paddy@99 467 PassphraseScheme: CurPassphraseScheme,
paddy@99 468 Created: time.Now(),
paddy@99 469 LastSeen: time.Now(),
paddy@99 470 }
paddy@99 471 err = context.SaveProfile(profile)
paddy@99 472 if err != nil {
paddy@105 473 if err == ErrProfileAlreadyExists {
paddy@105 474 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
paddy@105 475 return
paddy@105 476 }
paddy@105 477 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 478 return
paddy@99 479 }
paddy@99 480 logins := []Login{}
paddy@99 481 login := Login{
paddy@99 482 Type: "email",
paddy@99 483 Value: req.Email,
paddy@99 484 Created: profile.Created,
paddy@99 485 LastUsed: profile.Created,
paddy@99 486 ProfileID: profile.ID,
paddy@99 487 }
paddy@99 488 err = context.AddLogin(login)
paddy@99 489 if err != nil {
paddy@105 490 if err == ErrLoginAlreadyExists {
paddy@105 491 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
paddy@105 492 return
paddy@105 493 }
paddy@105 494 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 495 return
paddy@99 496 }
paddy@99 497 logins = append(logins, login)
paddy@99 498 if req.Username != "" {
paddy@99 499 login.Type = "username"
paddy@99 500 login.Value = req.Username
paddy@99 501 err = context.AddLogin(login)
paddy@99 502 if err != nil {
paddy@105 503 if err == ErrLoginAlreadyExists {
paddy@105 504 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/username"}}})
paddy@105 505 return
paddy@105 506 }
paddy@105 507 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 508 return
paddy@99 509 }
paddy@99 510 logins = append(logins, login)
paddy@99 511 }
paddy@105 512 resp := response{
paddy@105 513 Logins: logins,
paddy@105 514 Profiles: []Profile{profile},
paddy@105 515 }
paddy@105 516 encode(w, r, http.StatusCreated, resp)
paddy@99 517 // TODO(paddy): should we kick off the email validation flow?
paddy@99 518 }