auth

Paddy 2015-01-18 Parent:c03b5eb3179e Child:23c1a07c8a61

123:0a1e16b9c141 Go to Latest

auth/profile.go

Refactor verifyClient, implement refresh tokens. Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient out of each of the GrantType's validation functions and into the access token endpoint, where it will be called before the GrantType's validation function. Yay, less code repetition. And seeing as we always want to verify the client, that seems like a good way to prevent things like 118a69954621 from happening. This did, however, force us to add an AllowsPublic property to the GrantType, so the token endpoint knows whether or not a public Client is valid for any given GrantType. We also implemented the refresh token grant type, which required adding ClientID and RefreshRevoked as properties on the Token type. We need ClientID because we need to constrain refresh tokens to the client that issued them. We also should probably keep track of which tokens belong to which clients, just as a general rule of thumb. RefreshRevoked had to be created, next to Revoked, because the AccessToken could be revoked and the RefreshToken still valid, or vice versa. Notably, when you issue a new refresh token, the old one is revoked, but the access token is still valid. It remains to be seen whether this is a good way to track things or not. The number of duplicated properties lead me to believe our type is not a great representation of the underlying concepts.

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@105 428 }
paddy@105 429
paddy@99 430 // CreateProfileHandler is an HTTP handler for registering new profiles.
paddy@99 431 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@99 432 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@99 433 if !ok {
paddy@105 434 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 435 return
paddy@99 436 }
paddy@99 437 var req newProfileRequest
paddy@99 438 errors := []requestError{}
paddy@99 439 decoder := json.NewDecoder(r.Body)
paddy@99 440 err := decoder.Decode(&req)
paddy@99 441 if err != nil {
paddy@105 442 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@99 443 return
paddy@99 444 }
paddy@99 445 errors = append(errors, validateNewProfileRequest(&req)...)
paddy@99 446 if len(errors) > 0 {
paddy@105 447 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@99 448 return
paddy@99 449 }
paddy@99 450 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
paddy@99 451 if err != nil {
paddy@105 452 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 453 return
paddy@99 454 }
paddy@99 455 profile := Profile{
paddy@99 456 ID: uuid.NewID(),
paddy@99 457 Name: req.Name,
paddy@99 458 Passphrase: string(passphrase),
paddy@99 459 Iterations: context.config.iterations,
paddy@99 460 Salt: string(salt),
paddy@99 461 PassphraseScheme: CurPassphraseScheme,
paddy@99 462 Created: time.Now(),
paddy@99 463 LastSeen: time.Now(),
paddy@99 464 }
paddy@99 465 err = context.SaveProfile(profile)
paddy@99 466 if err != nil {
paddy@105 467 if err == ErrProfileAlreadyExists {
paddy@105 468 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
paddy@105 469 return
paddy@105 470 }
paddy@105 471 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 472 return
paddy@99 473 }
paddy@99 474 logins := []Login{}
paddy@99 475 login := Login{
paddy@99 476 Type: "email",
paddy@99 477 Value: req.Email,
paddy@99 478 Created: profile.Created,
paddy@99 479 LastUsed: profile.Created,
paddy@99 480 ProfileID: profile.ID,
paddy@99 481 }
paddy@99 482 err = context.AddLogin(login)
paddy@99 483 if err != nil {
paddy@105 484 if err == ErrLoginAlreadyExists {
paddy@105 485 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
paddy@105 486 return
paddy@105 487 }
paddy@105 488 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 489 return
paddy@99 490 }
paddy@99 491 logins = append(logins, login)
paddy@99 492 if req.Username != "" {
paddy@99 493 login.Type = "username"
paddy@99 494 login.Value = req.Username
paddy@99 495 err = context.AddLogin(login)
paddy@99 496 if err != nil {
paddy@105 497 if err == ErrLoginAlreadyExists {
paddy@105 498 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/username"}}})
paddy@105 499 return
paddy@105 500 }
paddy@105 501 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 502 return
paddy@99 503 }
paddy@99 504 logins = append(logins, login)
paddy@99 505 }
paddy@105 506 resp := response{
paddy@105 507 Logins: logins,
paddy@105 508 Profiles: []Profile{profile},
paddy@105 509 }
paddy@105 510 encode(w, r, http.StatusCreated, resp)
paddy@99 511 // TODO(paddy): should we kick off the email validation flow?
paddy@99 512 }