auth
auth/profile.go
Update import paths. Let's just use import paths that don't need aliasing, by specifying that they're mercurial repos.
| 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 } |