auth
172:8ecb60d29b0d Browse Files
Support email verification. The bulk of this commit is auto-modifying files to export variables (mostly our request error types and our response type) so that they can be reused in a Go client for that API. We also implement the beginnings of a Go client for that API, implementing the bare minimum we need for our immediate purposes: the ability to retrieve information about a Login. This, of course, means we need an API endpoint that will return information about a Login, which in turn required us to implement a GetLogin method in our profileStore. Which got in-memory and postgres implementations. That done, we could add the Verification field and Verified field to the Login type, to keep track of whether we've verified the user's ownership of those communication methods (if the Login is, in fact, a communication method). This required us to update sql/postgres_init.sql to account for the new fields we're tracking. It also means that when creating a Login, we had to generate a UUID to use as the Verification field. To make things complete, we needed a verifyLogin method on the profileStore to mark a Login as verified. That, in turn, required an endpoint to control this through the API. While doing so, I lumped things together in an UpdateLogin handler just so we could reuse the endpoint and logic when resending a verification email that may have never reached the user, for whatever reason (the quintessential "send again" button). Finally, we implemented an email_verification listener that will pull email_verification events off NSQ, check for the requisite data integrity, and use mailgun to email out a verification/welcome email.
.hgignore client.go client/client.go client/login.go client_test.go context.go listeners/email_verification/basic.html listeners/email_verification/basic.txt listeners/email_verification/listener.go profile.go profile_postgres.go profile_test.go request.go request_test.go session.go sql/postgres_init.sql
1.1 --- a/.hgignore Sun May 17 02:18:07 2015 -0400 1.2 +++ b/.hgignore Sun May 17 02:27:36 2015 -0400 1.3 @@ -1,2 +1,3 @@ 1.4 cover.out 1.5 authd/authd 1.6 +listeners/email_verification/email_verification
2.1 --- a/client.go Sun May 17 02:18:07 2015 -0400 2.2 +++ b/client.go Sun May 17 02:27:36 2015 -0400 2.3 @@ -455,22 +455,22 @@ 2.4 } 2.5 2.6 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { 2.7 - errors := []requestError{} 2.8 + errors := []RequestError{} 2.9 username, password, ok := r.BasicAuth() 2.10 if !ok { 2.11 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.12 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.13 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.14 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.15 return 2.16 } 2.17 profile, err := authenticate(username, password, c) 2.18 if err != nil { 2.19 if isAuthError(err) { 2.20 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.21 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.22 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.23 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.24 } else { 2.25 log.Printf("Error authenticating: %#+v\n", err) 2.26 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.27 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.28 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.29 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.30 } 2.31 return 2.32 } 2.33 @@ -482,19 +482,19 @@ 2.34 return 2.35 } 2.36 if req.Type == "" { 2.37 - errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"}) 2.38 + errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/type"}) 2.39 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential { 2.40 - errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"}) 2.41 + errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/type"}) 2.42 } 2.43 if req.Name == "" { 2.44 - errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"}) 2.45 + errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/name"}) 2.46 } else if len(req.Name) < minClientNameLen { 2.47 - errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) 2.48 + errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"}) 2.49 } else if len(req.Name) > maxClientNameLen { 2.50 - errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) 2.51 + errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"}) 2.52 } 2.53 if len(errors) > 0 { 2.54 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.55 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.56 return 2.57 } 2.58 client := Client{ 2.59 @@ -518,8 +518,8 @@ 2.60 err = c.SaveClient(client) 2.61 if err != nil { 2.62 if err == ErrClientAlreadyExists { 2.63 - errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"}) 2.64 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.65 + errors = append(errors, RequestError{Slug: RequestErrConflict, Field: "/id"}) 2.66 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.67 return 2.68 } 2.69 log.Printf("Error saving client: %#+v\n", err) 2.70 @@ -530,11 +530,11 @@ 2.71 for pos, u := range req.Endpoints { 2.72 uri, err := url.Parse(u) 2.73 if err != nil { 2.74 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) 2.75 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) 2.76 continue 2.77 } 2.78 if !uri.IsAbs() { 2.79 - errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)}) 2.80 + errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)}) 2.81 continue 2.82 } 2.83 endpoint := Endpoint{ 2.84 @@ -548,11 +548,11 @@ 2.85 err = c.AddEndpoints(endpoints) 2.86 if err != nil { 2.87 log.Printf("Error adding endpoints: %#+v\n", err) 2.88 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.89 - encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}}) 2.90 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.91 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors, Clients: []Client{client}}) 2.92 return 2.93 } 2.94 - resp := response{ 2.95 + resp := Response{ 2.96 Clients: []Client{client}, 2.97 Endpoints: endpoints, 2.98 Errors: errors, 2.99 @@ -561,28 +561,28 @@ 2.100 } 2.101 2.102 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) { 2.103 - errors := []requestError{} 2.104 + errors := []RequestError{} 2.105 vars := mux.Vars(r) 2.106 if vars["id"] == "" { 2.107 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 2.108 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.109 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 2.110 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.111 return 2.112 } 2.113 id, err := uuid.Parse(vars["id"]) 2.114 if err != nil { 2.115 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) 2.116 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.117 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) 2.118 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.119 return 2.120 } 2.121 client, err := c.GetClient(id) 2.122 if err != nil { 2.123 if err == ErrClientNotFound { 2.124 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 2.125 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 2.126 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) 2.127 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 2.128 return 2.129 } 2.130 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.131 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.132 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.133 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.134 return 2.135 } 2.136 username, password, ok := r.BasicAuth() 2.137 @@ -592,11 +592,11 @@ 2.138 profile, err := authenticate(username, password, c) 2.139 if err != nil { 2.140 if isAuthError(err) { 2.141 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.142 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.143 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.144 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.145 } else { 2.146 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.147 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.148 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.149 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.150 } 2.151 return 2.152 } 2.153 @@ -604,7 +604,7 @@ 2.154 client.Secret = "" 2.155 } 2.156 } 2.157 - resp := response{ 2.158 + resp := Response{ 2.159 Clients: []Client{client}, 2.160 Errors: errors, 2.161 } 2.162 @@ -612,7 +612,7 @@ 2.163 } 2.164 2.165 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) { 2.166 - errors := []requestError{} 2.167 + errors := []RequestError{} 2.168 var err error 2.169 // BUG(paddy): If ids are provided in query params, retrieve only those clients 2.170 num := defaultClientResponseSize 2.171 @@ -623,38 +623,38 @@ 2.172 if numStr != "" { 2.173 num, err = strconv.Atoi(numStr) 2.174 if err != nil { 2.175 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) 2.176 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"}) 2.177 } 2.178 if num > maxClientResponseSize { 2.179 - errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) 2.180 + errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"}) 2.181 } 2.182 if num < 1 { 2.183 - errors = append(errors, requestError{Slug: requestErrInsufficient, Param: "num"}) 2.184 + errors = append(errors, RequestError{Slug: RequestErrInsufficient, Param: "num"}) 2.185 } 2.186 } 2.187 if offsetStr != "" { 2.188 offset, err = strconv.Atoi(offsetStr) 2.189 if err != nil { 2.190 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) 2.191 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"}) 2.192 } 2.193 } 2.194 if ownerIDStr == "" { 2.195 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"}) 2.196 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "owner_id"}) 2.197 } 2.198 if len(errors) > 0 { 2.199 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.200 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.201 return 2.202 } 2.203 ownerID, err := uuid.Parse(ownerIDStr) 2.204 if err != nil { 2.205 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"}) 2.206 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.207 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "owner_id"}) 2.208 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.209 return 2.210 } 2.211 clients, err := c.ListClientsByOwner(ownerID, num, offset) 2.212 if err != nil { 2.213 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.214 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.215 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.216 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.217 return 2.218 } 2.219 username, password, ok := r.BasicAuth() 2.220 @@ -667,11 +667,11 @@ 2.221 profile, err := authenticate(username, password, c) 2.222 if err != nil { 2.223 if isAuthError(err) { 2.224 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.225 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.226 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.227 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.228 } else { 2.229 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.230 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.231 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.232 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.233 } 2.234 return 2.235 } 2.236 @@ -682,7 +682,7 @@ 2.237 } 2.238 } 2.239 } 2.240 - resp := response{ 2.241 + resp := Response{ 2.242 Clients: clients, 2.243 Errors: errors, 2.244 } 2.245 @@ -690,80 +690,80 @@ 2.246 } 2.247 2.248 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { 2.249 - errors := []requestError{} 2.250 + errors := []RequestError{} 2.251 vars := mux.Vars(r) 2.252 if _, ok := vars["id"]; !ok { 2.253 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 2.254 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.255 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 2.256 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.257 return 2.258 } 2.259 id, err := uuid.Parse(vars["id"]) 2.260 if err != nil { 2.261 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) 2.262 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) 2.263 } 2.264 username, password, ok := r.BasicAuth() 2.265 if !ok { 2.266 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.267 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.268 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.269 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.270 return 2.271 } 2.272 profile, err := authenticate(username, password, c) 2.273 if err != nil { 2.274 if isAuthError(err) { 2.275 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.276 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.277 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.278 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.279 } else { 2.280 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.281 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.282 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.283 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.284 } 2.285 return 2.286 } 2.287 var change ClientChange 2.288 err = decode(r, &change) 2.289 if err != nil { 2.290 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"}) 2.291 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.292 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/"}) 2.293 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.294 return 2.295 } 2.296 errs := change.Validate() 2.297 for _, err := range errs { 2.298 switch err { 2.299 case ErrEmptyChange: 2.300 - errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"}) 2.301 + errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"}) 2.302 case ErrClientNameTooShort: 2.303 - errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) 2.304 + errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"}) 2.305 case ErrClientNameTooLong: 2.306 - errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) 2.307 + errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"}) 2.308 case ErrClientLogoTooLong: 2.309 - errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"}) 2.310 + errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/logo"}) 2.311 case ErrClientLogoNotURL: 2.312 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"}) 2.313 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/logo"}) 2.314 case ErrClientWebsiteTooLong: 2.315 - errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"}) 2.316 + errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/website"}) 2.317 case ErrClientWebsiteNotURL: 2.318 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"}) 2.319 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/website"}) 2.320 default: 2.321 log.Println("Unrecognised error from client change validation:", err) 2.322 } 2.323 } 2.324 if len(errors) > 0 { 2.325 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.326 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.327 return 2.328 } 2.329 client, err := c.GetClient(id) 2.330 if err == ErrClientNotFound { 2.331 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 2.332 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 2.333 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) 2.334 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 2.335 return 2.336 } else if err != nil { 2.337 log.Println("Error retrieving client:", err) 2.338 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.339 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.340 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.341 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.342 return 2.343 } 2.344 if !client.OwnerID.Equal(profile.ID) { 2.345 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.346 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 2.347 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.348 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 2.349 return 2.350 } 2.351 if change.Secret != nil && client.Type == clientTypeConfidential { 2.352 @@ -779,59 +779,59 @@ 2.353 err = c.UpdateClient(id, change) 2.354 if err != nil { 2.355 log.Println("Error updating client:", err) 2.356 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.357 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.358 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.359 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.360 return 2.361 } 2.362 client.ApplyChange(change) 2.363 - encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors}) 2.364 + encode(w, r, http.StatusOK, Response{Clients: []Client{client}, Errors: errors}) 2.365 return 2.366 } 2.367 2.368 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) { 2.369 - errors := []requestError{} 2.370 + errors := []RequestError{} 2.371 vars := mux.Vars(r) 2.372 if _, ok := vars["id"]; !ok { 2.373 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 2.374 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 2.375 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 2.376 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 2.377 return 2.378 } 2.379 id, err := uuid.Parse(vars["id"]) 2.380 if err != nil { 2.381 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 2.382 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) 2.383 } 2.384 username, password, ok := r.BasicAuth() 2.385 if !ok { 2.386 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.387 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.388 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.389 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.390 return 2.391 } 2.392 profile, err := authenticate(username, password, c) 2.393 if err != nil { 2.394 if isAuthError(err) { 2.395 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.396 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.397 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.398 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.399 } else { 2.400 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.401 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.402 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.403 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.404 } 2.405 return 2.406 } 2.407 client, err := c.GetClient(id) 2.408 if err != nil { 2.409 if err == ErrClientNotFound { 2.410 - errors = append(errors, requestError{Slug: requestErrNotFound}) 2.411 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 2.412 + errors = append(errors, RequestError{Slug: RequestErrNotFound}) 2.413 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 2.414 return 2.415 } 2.416 log.Println("Error retrieving client:", err) 2.417 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.418 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.419 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.420 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.421 return 2.422 } 2.423 if !client.OwnerID.Equal(profile.ID) { 2.424 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.425 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 2.426 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.427 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 2.428 return 2.429 } 2.430 deleted := true 2.431 @@ -839,16 +839,16 @@ 2.432 err = c.UpdateClient(id, change) 2.433 if err != nil { 2.434 if err == ErrClientNotFound { 2.435 - errors = append(errors, requestError{Slug: requestErrNotFound}) 2.436 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 2.437 + errors = append(errors, RequestError{Slug: RequestErrNotFound}) 2.438 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 2.439 return 2.440 } 2.441 log.Println("Error deleting client:", err) 2.442 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.443 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.444 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.445 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.446 return 2.447 } 2.448 - encode(w, r, http.StatusOK, response{Errors: errors}) 2.449 + encode(w, r, http.StatusOK, Response{Errors: errors}) 2.450 go cleanUpAfterClientDeletion(id, c) 2.451 return 2.452 } 2.453 @@ -857,50 +857,50 @@ 2.454 type addEndpointReq struct { 2.455 Endpoints []string `json:"endpoints"` 2.456 } 2.457 - errors := []requestError{} 2.458 + errors := []RequestError{} 2.459 vars := mux.Vars(r) 2.460 if vars["id"] == "" { 2.461 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 2.462 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.463 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 2.464 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.465 return 2.466 } 2.467 id, err := uuid.Parse(vars["id"]) 2.468 if err != nil { 2.469 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) 2.470 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.471 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) 2.472 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.473 return 2.474 } 2.475 username, password, ok := r.BasicAuth() 2.476 if !ok { 2.477 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.478 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.479 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.480 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.481 return 2.482 } 2.483 profile, err := authenticate(username, password, c) 2.484 if err != nil { 2.485 if isAuthError(err) { 2.486 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.487 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.488 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.489 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.490 } else { 2.491 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.492 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.493 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.494 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.495 } 2.496 return 2.497 } 2.498 client, err := c.GetClient(id) 2.499 if err != nil { 2.500 if err == ErrClientNotFound { 2.501 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 2.502 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.503 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) 2.504 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.505 return 2.506 } 2.507 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.508 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.509 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.510 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.511 return 2.512 } 2.513 if !client.OwnerID.Equal(profile.ID) { 2.514 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.515 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.516 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.517 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.518 return 2.519 } 2.520 var req addEndpointReq 2.521 @@ -911,17 +911,17 @@ 2.522 return 2.523 } 2.524 if len(req.Endpoints) < 1 { 2.525 - errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"}) 2.526 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.527 + errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/endpoints"}) 2.528 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.529 return 2.530 } 2.531 endpoints := []Endpoint{} 2.532 for pos, u := range req.Endpoints { 2.533 if parsed, err := url.Parse(u); err != nil { 2.534 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) 2.535 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) 2.536 continue 2.537 } else if !parsed.IsAbs() { 2.538 - errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)}) 2.539 + errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)}) 2.540 continue 2.541 } 2.542 e := Endpoint{ 2.543 @@ -933,7 +933,7 @@ 2.544 endpoints = append(endpoints, e) 2.545 } 2.546 if len(errors) > 0 { 2.547 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.548 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.549 return 2.550 } 2.551 err = c.AddEndpoints(endpoints) 2.552 @@ -941,7 +941,7 @@ 2.553 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 2.554 return 2.555 } 2.556 - resp := response{ 2.557 + resp := Response{ 2.558 Errors: errors, 2.559 Endpoints: endpoints, 2.560 } 2.561 @@ -949,12 +949,12 @@ 2.562 } 2.563 2.564 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { 2.565 - errors := []requestError{} 2.566 + errors := []RequestError{} 2.567 vars := mux.Vars(r) 2.568 clientID, err := uuid.Parse(vars["id"]) 2.569 if err != nil { 2.570 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"}) 2.571 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.572 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"}) 2.573 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.574 return 2.575 } 2.576 num := defaultEndpointResponseSize 2.577 @@ -964,29 +964,29 @@ 2.578 if numStr != "" { 2.579 num, err = strconv.Atoi(numStr) 2.580 if err != nil { 2.581 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) 2.582 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"}) 2.583 } 2.584 if num > maxEndpointResponseSize { 2.585 - errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) 2.586 + errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"}) 2.587 } 2.588 } 2.589 if offsetStr != "" { 2.590 offset, err = strconv.Atoi(offsetStr) 2.591 if err != nil { 2.592 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) 2.593 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"}) 2.594 } 2.595 } 2.596 if len(errors) > 0 { 2.597 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.598 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.599 return 2.600 } 2.601 endpoints, err := c.ListEndpoints(clientID, num, offset) 2.602 if err != nil { 2.603 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.604 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.605 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.606 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.607 return 2.608 } 2.609 - resp := response{ 2.610 + resp := Response{ 2.611 Endpoints: endpoints, 2.612 Errors: errors, 2.613 } 2.614 @@ -994,72 +994,72 @@ 2.615 } 2.616 2.617 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) { 2.618 - errors := []requestError{} 2.619 + errors := []RequestError{} 2.620 vars := mux.Vars(r) 2.621 if vars["client_id"] == "" { 2.622 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"}) 2.623 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.624 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "client_id"}) 2.625 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.626 return 2.627 } 2.628 clientID, err := uuid.Parse(vars["client_id"]) 2.629 if err != nil { 2.630 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"}) 2.631 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.632 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"}) 2.633 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.634 return 2.635 } 2.636 if vars["id"] == "" { 2.637 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 2.638 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.639 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 2.640 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.641 return 2.642 } 2.643 id, err := uuid.Parse(vars["id"]) 2.644 if err != nil { 2.645 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) 2.646 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.647 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) 2.648 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.649 return 2.650 } 2.651 username, password, ok := r.BasicAuth() 2.652 if !ok { 2.653 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.654 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.655 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.656 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.657 return 2.658 } 2.659 profile, err := authenticate(username, password, c) 2.660 if err != nil { 2.661 if isAuthError(err) { 2.662 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.663 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.664 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.665 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.666 } else { 2.667 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.668 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.669 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.670 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.671 } 2.672 return 2.673 } 2.674 client, err := c.GetClient(clientID) 2.675 if err != nil { 2.676 if err == ErrClientNotFound { 2.677 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"}) 2.678 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.679 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "client_id"}) 2.680 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.681 return 2.682 } 2.683 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.684 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.685 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.686 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.687 return 2.688 } 2.689 if !client.OwnerID.Equal(profile.ID) { 2.690 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 2.691 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 2.692 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 2.693 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 2.694 return 2.695 } 2.696 endpoint, err := c.GetEndpoint(clientID, id) 2.697 if err != nil { 2.698 if err == ErrEndpointNotFound { 2.699 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 2.700 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 2.701 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) 2.702 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 2.703 return 2.704 } 2.705 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 2.706 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 2.707 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 2.708 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 2.709 return 2.710 } 2.711 err = c.RemoveEndpoint(clientID, id) 2.712 @@ -1067,7 +1067,7 @@ 2.713 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 2.714 return 2.715 } 2.716 - resp := response{ 2.717 + resp := Response{ 2.718 Errors: errors, 2.719 Endpoints: []Endpoint{endpoint}, 2.720 }
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/client/client.go Sun May 17 02:27:36 2015 -0400 3.3 @@ -0,0 +1,81 @@ 3.4 +package client 3.5 + 3.6 +import ( 3.7 + "bytes" 3.8 + "encoding/json" 3.9 + "errors" 3.10 + "io" 3.11 + "net/http" 3.12 + "strings" 3.13 + 3.14 + "code.secondbit.org/auth.hg" 3.15 +) 3.16 + 3.17 +var ( 3.18 + ErrNilClient = errors.New("nil client wrapper") 3.19 + ErrNilHTTPClient = errors.New("nil client") 3.20 +) 3.21 + 3.22 +type Client struct { 3.23 + client *http.Client 3.24 + address string 3.25 +} 3.26 + 3.27 +func New(address string) *Client { 3.28 + address = strings.TrimRight(address, "/") 3.29 + return &Client{ 3.30 + address: address, 3.31 + client: &http.Client{}, 3.32 + } 3.33 +} 3.34 + 3.35 +func (c *Client) do(method, url string, request interface{}) (auth.Response, error) { 3.36 + if c == nil { 3.37 + return auth.Response{}, ErrNilClient 3.38 + } 3.39 + if c.client == nil { 3.40 + return auth.Response{}, ErrNilHTTPClient 3.41 + } 3.42 + var response auth.Response 3.43 + if !strings.HasPrefix(url, "http") { 3.44 + url = strings.TrimLeft(url, "/") 3.45 + url = "/" + url 3.46 + url = c.address + url 3.47 + } 3.48 + var body io.Reader 3.49 + if request != nil { 3.50 + data, err := json.Marshal(request) 3.51 + if err != nil { 3.52 + return response, err 3.53 + } 3.54 + body = bytes.NewBuffer(data) 3.55 + } 3.56 + req, err := http.NewRequest(method, url, body) 3.57 + if err != nil { 3.58 + return response, err 3.59 + } 3.60 + resp, err := c.client.Do(req) 3.61 + if err != nil { 3.62 + return response, err 3.63 + } 3.64 + defer resp.Body.Close() 3.65 + switch resp.Header.Get("Content-Type") { 3.66 + case "application/json": 3.67 + dec := json.NewDecoder(resp.Body) 3.68 + err = dec.Decode(&response) 3.69 + if err != nil { 3.70 + return response, err 3.71 + } 3.72 + default: 3.73 + dec := json.NewDecoder(resp.Body) 3.74 + err = dec.Decode(&response) 3.75 + if err != nil { 3.76 + return response, err 3.77 + } 3.78 + } 3.79 + return response, nil 3.80 +} 3.81 + 3.82 +func (c *Client) Get(url string) (auth.Response, error) { 3.83 + return c.do("GET", url, nil) 3.84 +}
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/client/login.go Sun May 17 02:27:36 2015 -0400 4.3 @@ -0,0 +1,16 @@ 4.4 +package client 4.5 + 4.6 +import ( 4.7 + "code.secondbit.org/auth.hg" 4.8 +) 4.9 + 4.10 +func (c *Client) GetLogin(value string) (auth.Login, error) { 4.11 + resp, err := c.Get("/logins/" + value) 4.12 + if err != nil { 4.13 + return auth.Login{}, err 4.14 + } 4.15 + if len(resp.Logins) < 1 { 4.16 + return auth.Login{}, auth.ErrLoginNotFound 4.17 + } 4.18 + return resp.Logins[0], nil 4.19 +}
5.1 --- a/client_test.go Sun May 17 02:18:07 2015 -0400 5.2 +++ b/client_test.go Sun May 17 02:27:36 2015 -0400 5.3 @@ -756,24 +756,24 @@ 5.4 type testStruct struct { 5.5 request string 5.6 code int 5.7 - resp response 5.8 + resp Response 5.9 } 5.10 tests := []testStruct{ 5.11 - {``, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/"}}}}, 5.12 - {`{}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}}, 5.13 - {`{"type":"notarealtype"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}}, 5.14 - {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrOverflow, Field: "/name"}}}}, 5.15 - {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrInsufficient, Field: "/name"}}}}, 5.16 - {`{"type":"public"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/name"}}}}, 5.17 - {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrOverflow, Field: "/name"}}}}, 5.18 - {`{"type":"public","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInsufficient, Field: "/name"}}}}, 5.19 - {`{"name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}}}}, 5.20 - {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}}}}, 5.21 - {`{"type":"public","name":"My Client"}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}}, 5.22 - {`{"type":"public","name":"My Client", "endpoints": ["https://test.secondbit.org/", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://test.secondbit.org/"}, {URI: "https://paddy.io"}}}}, 5.23 - {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/endpoints/0"}}}}, 5.24 - {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "/relative/uri", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/endpoints/0"}, {Slug: requestErrInvalidValue, Field: "/endpoints/1"}}}}, 5.25 - {`{"type":"confidential","name":"Secret Client", "endpoints": ["https://secondbit.org"]}`, http.StatusCreated, response{Clients: []Client{{Name: "Secret Client", OwnerID: profile.ID, Type: "confidential"}}, Endpoints: []Endpoint{{URI: "https://secondbit.org"}}}}, 5.26 + {``, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidFormat, Field: "/"}}}}, 5.27 + {`{}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrMissing, Field: "/type"}, {Slug: RequestErrMissing, Field: "/name"}}}}, 5.28 + {`{"type":"notarealtype"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}, {Slug: RequestErrMissing, Field: "/name"}}}}, 5.29 + {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}, {Slug: RequestErrOverflow, Field: "/name"}}}}, 5.30 + {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}, {Slug: RequestErrInsufficient, Field: "/name"}}}}, 5.31 + {`{"type":"public"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrMissing, Field: "/name"}}}}, 5.32 + {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrOverflow, Field: "/name"}}}}, 5.33 + {`{"type":"public","name":"a"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInsufficient, Field: "/name"}}}}, 5.34 + {`{"name":"My Client"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrMissing, Field: "/type"}}}}, 5.35 + {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}}}}, 5.36 + {`{"type":"public","name":"My Client"}`, http.StatusCreated, Response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}}, 5.37 + {`{"type":"public","name":"My Client", "endpoints": ["https://test.secondbit.org/", "https://paddy.io"]}`, http.StatusCreated, Response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://test.secondbit.org/"}, {URI: "https://paddy.io"}}}}, 5.38 + {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "https://paddy.io"]}`, http.StatusCreated, Response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []RequestError{{Slug: RequestErrInvalidFormat, Field: "/endpoints/0"}}}}, 5.39 + {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "/relative/uri", "https://paddy.io"]}`, http.StatusCreated, Response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []RequestError{{Slug: RequestErrInvalidFormat, Field: "/endpoints/0"}, {Slug: RequestErrInvalidValue, Field: "/endpoints/1"}}}}, 5.40 + {`{"type":"confidential","name":"Secret Client", "endpoints": ["https://secondbit.org"]}`, http.StatusCreated, Response{Clients: []Client{{Name: "Secret Client", OwnerID: profile.ID, Type: "confidential"}}, Endpoints: []Endpoint{{URI: "https://secondbit.org"}}}}, 5.41 } 5.42 for pos, test := range tests { 5.43 t.Logf("Test #%d: `%s`", pos, test.request) 5.44 @@ -785,7 +785,7 @@ 5.45 t.Errorf("Expected response code to be %d, got %d", test.code, w.Code) 5.46 } 5.47 t.Logf("Response: %s", w.Body.String()) 5.48 - var res response 5.49 + var res Response 5.50 err = json.Unmarshal(w.Body.Bytes(), &res) 5.51 if err != nil { 5.52 t.Error("Unexpected error unmarshalling response:", err) 5.53 @@ -870,7 +870,7 @@ 5.54 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code) 5.55 } 5.56 t.Logf("Response: %s", w.Body.String()) 5.57 - var res response 5.58 + var res Response 5.59 err = json.Unmarshal(w.Body.Bytes(), &res) 5.60 if err != nil { 5.61 t.Error("Unexpected error unmarshalling response:", err) 5.62 @@ -901,7 +901,7 @@ 5.63 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code) 5.64 } 5.65 t.Logf("Response: %s", w.Body.String()) 5.66 - res = response{} 5.67 + res = Response{} 5.68 err = json.Unmarshal(w.Body.Bytes(), &res) 5.69 if err != nil { 5.70 t.Error("Unexpected error unmarshalling response:", err) 5.71 @@ -909,7 +909,7 @@ 5.72 if len(res.Errors) != 1 { 5.73 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors)) 5.74 } 5.75 - e := requestError{Slug: requestErrInvalidFormat, Param: "id"} 5.76 + e := RequestError{Slug: RequestErrInvalidFormat, Param: "id"} 5.77 success, field, expectation, result = compareErrors(e, res.Errors[0]) 5.78 if !success { 5.79 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result) 5.80 @@ -928,7 +928,7 @@ 5.81 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code) 5.82 } 5.83 t.Logf("Response: %s", w.Body.String()) 5.84 - res = response{} 5.85 + res = Response{} 5.86 err = json.Unmarshal(w.Body.Bytes(), &res) 5.87 if err != nil { 5.88 t.Error("Unexpected error unmarshalling response:", err) 5.89 @@ -936,7 +936,7 @@ 5.90 if len(res.Errors) != 1 { 5.91 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors)) 5.92 } 5.93 - e = requestError{Slug: requestErrNotFound, Param: "id"} 5.94 + e = RequestError{Slug: RequestErrNotFound, Param: "id"} 5.95 success, field, expectation, result = compareErrors(e, res.Errors[0]) 5.96 if !success { 5.97 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result) 5.98 @@ -1036,7 +1036,7 @@ 5.99 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code) 5.100 } 5.101 t.Logf("Response: %s", w.Body.String()) 5.102 - var res response 5.103 + var res Response 5.104 err = json.Unmarshal(w.Body.Bytes(), &res) 5.105 if err != nil { 5.106 t.Error("Unexpected error unmarshalling response:", err) 5.107 @@ -1063,7 +1063,7 @@ 5.108 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code) 5.109 } 5.110 t.Logf("Response: %s", w.Body.String()) 5.111 - res = response{} 5.112 + res = Response{} 5.113 err = json.Unmarshal(w.Body.Bytes(), &res) 5.114 if err != nil { 5.115 t.Error("Unexpected error unmarshalling response:", err) 5.116 @@ -1071,7 +1071,7 @@ 5.117 if len(res.Errors) != 1 { 5.118 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors)) 5.119 } 5.120 - e := requestError{Slug: requestErrInvalidFormat, Param: "id"} 5.121 + e := RequestError{Slug: RequestErrInvalidFormat, Param: "id"} 5.122 success, field, expectation, result = compareErrors(e, res.Errors[0]) 5.123 if !success { 5.124 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result) 5.125 @@ -1091,7 +1091,7 @@ 5.126 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code) 5.127 } 5.128 t.Logf("Response: %s", w.Body.String()) 5.129 - res = response{} 5.130 + res = Response{} 5.131 err = json.Unmarshal(w.Body.Bytes(), &res) 5.132 if err != nil { 5.133 t.Error("Unexpected error unmarshalling response:", err) 5.134 @@ -1099,7 +1099,7 @@ 5.135 if len(res.Errors) != 1 { 5.136 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors)) 5.137 } 5.138 - e = requestError{Slug: requestErrNotFound, Param: "id"} 5.139 + e = RequestError{Slug: RequestErrNotFound, Param: "id"} 5.140 success, field, expectation, result = compareErrors(e, res.Errors[0]) 5.141 if !success { 5.142 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result) 5.143 @@ -1119,7 +1119,7 @@ 5.144 t.Errorf("Expected response code to be %d, got %d", http.StatusUnauthorized, w.Code) 5.145 } 5.146 t.Logf("Response: %s", w.Body.String()) 5.147 - res = response{} 5.148 + res = Response{} 5.149 err = json.Unmarshal(w.Body.Bytes(), &res) 5.150 if err != nil { 5.151 t.Error("Unexpected error unmarshalling response:", err) 5.152 @@ -1127,7 +1127,7 @@ 5.153 if len(res.Errors) != 1 { 5.154 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors)) 5.155 } 5.156 - e = requestError{Slug: requestErrAccessDenied} 5.157 + e = RequestError{Slug: RequestErrAccessDenied} 5.158 success, field, expectation, result = compareErrors(e, res.Errors[0]) 5.159 if !success { 5.160 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result) 5.161 @@ -1147,7 +1147,7 @@ 5.162 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code) 5.163 } 5.164 t.Logf("Response: %s", w.Body.String()) 5.165 - res = response{} 5.166 + res = Response{} 5.167 err = json.Unmarshal(w.Body.Bytes(), &res) 5.168 if err != nil { 5.169 t.Error("Unexpected error unmarshalling response:", err)
6.1 --- a/context.go Sun May 17 02:18:07 2015 -0400 6.2 +++ b/context.go Sun May 17 02:27:36 2015 -0400 6.3 @@ -14,15 +14,16 @@ 6.4 // be used as the main point of interaction for the data storage 6.5 // layer. 6.6 type Context struct { 6.7 - template *template.Template 6.8 - loginURI *url.URL 6.9 - clients clientStore 6.10 - authCodes authorizationCodeStore 6.11 - profiles profileStore 6.12 - tokens tokenStore 6.13 - sessions sessionStore 6.14 - scopes scopeStore 6.15 - config Config 6.16 + template *template.Template 6.17 + loginURI *url.URL 6.18 + clients clientStore 6.19 + authCodes authorizationCodeStore 6.20 + profiles profileStore 6.21 + tokens tokenStore 6.22 + sessions sessionStore 6.23 + scopes scopeStore 6.24 + loginVerificationNotifier loginVerificationNotifier 6.25 + config Config 6.26 } 6.27 6.28 // NewContext takes a Config instance and uses it to bootstrap a Context 6.29 @@ -38,8 +39,9 @@ 6.30 tokens: config.TokenStore, 6.31 sessions: config.SessionStore, 6.32 scopes: config.ScopeStore, 6.33 - template: config.Template, 6.34 - config: config, 6.35 + loginVerificationNotifier: config.LoginVerificationNotifier, 6.36 + template: config.Template, 6.37 + config: config, 6.38 } 6.39 var err error 6.40 context.loginURI, err = url.Parse(config.LoginURI) 6.41 @@ -293,6 +295,14 @@ 6.42 return c.profiles.addLogin(login) 6.43 } 6.44 6.45 +// GetLogin returns the Login specified from the profileStore associated with the Context. 6.46 +func (c Context) GetLogin(value string) (Login, error) { 6.47 + if c.profiles == nil { 6.48 + return Login{}, ErrNoProfileStore 6.49 + } 6.50 + return c.profiles.getLogin(value) 6.51 +} 6.52 + 6.53 // RemoveLogin removes the specified Login from the profileStore associated with the Context, provided 6.54 // the Login has a ProfileID property that matches the profile ID passed in. It also disassociates the 6.55 // deleted Login from the Profile in login.ProfileID. 6.56 @@ -321,6 +331,16 @@ 6.57 return c.profiles.recordLoginUse(value, when) 6.58 } 6.59 6.60 +// VerifyLogin sets the Verified property of the Login specied to true in the profileStore associated with 6.61 +// the Context, assuming the Verification property of the Login in the profileStore matches the verification 6.62 +// passed. If the verifications do not match, an ErrLoginVerificationInvalid error is returned. 6.63 +func (c Context) VerifyLogin(value, verification string) error { 6.64 + if c.profiles == nil { 6.65 + return ErrNoProfileStore 6.66 + } 6.67 + return c.profiles.verifyLogin(value, verification) 6.68 +} 6.69 + 6.70 // ListLogins returns a slice of up to num Logins associated with the specified Profile from the profileStore 6.71 // associated with the Context, skipping offset Profiles. 6.72 func (c Context) ListLogins(profile uuid.ID, num, offset int) ([]Login, error) { 6.73 @@ -470,3 +490,10 @@ 6.74 } 6.75 return c.scopes.listScopes() 6.76 } 6.77 + 6.78 +func (c Context) SendLoginVerification(login Login) { 6.79 + if c.loginVerificationNotifier == nil { 6.80 + log.Println("Login verification notifier not set!") 6.81 + } 6.82 + c.loginVerificationNotifier.SendLoginVerification(login) 6.83 +}
7.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 7.2 +++ b/listeners/email_verification/basic.html Sun May 17 02:27:36 2015 -0400 7.3 @@ -0,0 +1,15 @@ 7.4 +<html> 7.5 + <body> 7.6 + <p>Hello, friend!</p> 7.7 + <p>Welcome to Ducky! We’re so glad to meet you. We’re excited. Are you excited? Of course you’re excited. This is exciting!</p> 7.8 + <p>Could you do us a quick favour and just verify this email address? We just want to know that we can reach you, in case you forget your password, something goes wrong with billing, or (knock on wood) we have a security problem we need to tell you about.</p> 7.9 + <p>Verifying your email can be really quick. Just <a href="http://useducky.com/logins/verify?code={{ .login.Verification }}">click here</a> or enter your verification code on the website or in one of the apps:</p> 7.10 + <div stlye="text-align: center;">{{ .login.Verification }}</div> 7.11 + <p>Thanks a tonne. You’re the best. We’re going to get along <em>famously</em>. Let’s be best friends.</p> 7.12 + <p>This is going <em>swimmingly</em>. (Just get used to the duck puns.)</p> 7.13 + <p>Hugs,<br/> 7.14 + Team Ducky</p> 7.15 + <p>PS: We’re friendly. This is <em>not</em> one of those “noreply” email addresses. We would actually be <em>really ecstatic</em> if you replied. We weren’t kidding about that whole “best friends” thing.</p> 7.16 + <p>PPS: Look, if email’s not really your thing, we get it. You can also call us: (716) 771-2486. We’d love to chat with you.</p> 7.17 + </body> 7.18 +</html>
8.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 8.2 +++ b/listeners/email_verification/basic.txt Sun May 17 02:27:36 2015 -0400 8.3 @@ -0,0 +1,18 @@ 8.4 +Hello, friend! 8.5 + 8.6 +Welcome to Ducky! We’re so glad to meet you. We’re excited. Are you excited? Of course you’re excited. This is exciting! 8.7 + 8.8 +Could you do us a quick favour and just verify this email address? We just want to know that we can reach you, in case you forget your password, something goes wrong with billing, or (knock on wood) we have a security problem we need to tell you about. 8.9 + 8.10 +Verifying your email can be really quick. Just enter your verification code: {{ .login.Verification }} 8.11 + 8.12 +Thanks a tonne. You’re the best. We’re going to get along famously. Let’s be best friends. 8.13 + 8.14 +This is going swimmingly. (Just get used to the duck puns.) 8.15 + 8.16 +Hugs, 8.17 +Team Ducky 8.18 + 8.19 +PS: We’re friendly. This is not one of those “noreply” email addresses. We would actually be really ecstatic if you replied. We weren’t kidding about that whole “best friends” thing. 8.20 + 8.21 +PPS: Look, if email’s not really your thing, we get it. You can also call us: (716) 771-2486. We’d love to chat with you.
9.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 9.2 +++ b/listeners/email_verification/listener.go Sun May 17 02:27:36 2015 -0400 9.3 @@ -0,0 +1,134 @@ 9.4 +package main 9.5 + 9.6 +import ( 9.7 + "bytes" 9.8 + "encoding/json" 9.9 + "fmt" 9.10 + htmltmpl "html/template" 9.11 + "log" 9.12 + "os" 9.13 + "strings" 9.14 + texttmpl "text/template" 9.15 + 9.16 + "code.secondbit.org/auth.hg" 9.17 + "code.secondbit.org/auth.hg/client" 9.18 + "code.secondbit.org/events.hg" 9.19 + 9.20 + "github.com/bitly/go-nsq" 9.21 + "github.com/mailgun/mailgun-go" 9.22 +) 9.23 + 9.24 +const ( 9.25 + channel = "verification_email_sender" 9.26 +) 9.27 + 9.28 +var ( 9.29 + authClient *client.Client 9.30 + mgClient mailgun.Mailgun 9.31 + emailHTMLTemplate *htmltmpl.Template 9.32 + emailTextTemplate *texttmpl.Template 9.33 +) 9.34 + 9.35 +func main() { 9.36 + lookupdEnv := os.Getenv("AUTH_NSQ_LOOKUPDS") 9.37 + if lookupdEnv == "" { 9.38 + log.Fatal("AUTH_NSQ_LOOKUPDS environment variable must be set.") 9.39 + } 9.40 + lookupds := strings.Split(lookupdEnv, ",") 9.41 + clientAddr := os.Getenv("AUTH_API") 9.42 + if clientAddr == "" { 9.43 + log.Fatal("AUTH_API environment variable must be set.") 9.44 + } 9.45 + mgDomain := os.Getenv("AUTH_EMAIL_DOMAIN") 9.46 + if mgDomain == "" { 9.47 + log.Fatal("AUTH_EMAIL_DOMAIN environment variable must be set.") 9.48 + } 9.49 + mgAPIKey := os.Getenv("AUTH_EMAIL_KEY") 9.50 + if mgAPIKey == "" { 9.51 + log.Fatal("AUTH_EMAIL_KEY environment variable must be set.") 9.52 + } 9.53 + mgHTMLTemplateFile := os.Getenv("AUTH_EMAIL_HTML_TEMPLATE") 9.54 + if mgHTMLTemplateFile == "" { 9.55 + log.Fatal("AUTH_EMAIL_HTML_TEMPLATE environment variable must be set.") 9.56 + } 9.57 + mgTextTemplateFile := os.Getenv("AUTH_EMAIL_TEXT_TEMPLATE") 9.58 + if mgTextTemplateFile == "" { 9.59 + log.Fatal("AUTH_EMAIL_TEXT_TEMPLATE environment variable must be set.") 9.60 + } 9.61 + authClient = client.New(clientAddr) 9.62 + mgClient = mailgun.NewMailgun(mgDomain, mgAPIKey, "") 9.63 + emailHTMLTemplate = htmltmpl.Must(htmltmpl.ParseGlob(mgHTMLTemplateFile)) 9.64 + emailTextTemplate = texttmpl.Must(texttmpl.ParseGlob(mgTextTemplateFile)) 9.65 + sub, err := events.NewNSQSubscriber(lookupds, nsq.HandlerFunc(messageHandler), auth.EventTopicLoginVerification, channel, "email_verification/"+auth.Version) 9.66 + if err != nil { 9.67 + log.Fatalf("Error creating subscriber: %+v\n", err) 9.68 + } 9.69 + sub.Block() 9.70 +} 9.71 + 9.72 +func messageHandler(msg *nsq.Message) error { 9.73 + var event events.Event 9.74 + err := json.Unmarshal(msg.Body, &event) 9.75 + if err != nil { 9.76 + log.Printf("Error decoding event (%s), discarding: %+v\n", err) 9.77 + return nil 9.78 + } 9.79 + if event.System != auth.EventSystem { 9.80 + log.Printf("Ignoring event originating from %s\n", event.System) 9.81 + return nil 9.82 + } 9.83 + if event.Model != auth.EventModelLogin { 9.84 + log.Printf("Ignoring event for model %s\n", event.Model) 9.85 + return nil 9.86 + } 9.87 + if event.Action != auth.EventActionSendVerification { 9.88 + log.Printf("Ignoring event caused by %s\n", event.Action) 9.89 + return nil 9.90 + } 9.91 + if event.Data == nil { 9.92 + log.Printf("Ignoring event with data not set.\n") 9.93 + return nil 9.94 + } 9.95 + data, ok := event.Data.(map[string]interface{}) 9.96 + if !ok { 9.97 + log.Printf("Ignoring event with data not map[string]string.\n") 9.98 + return nil 9.99 + } 9.100 + if _, ok := data["verification"]; !ok { 9.101 + log.Printf("Ignoring event with no verification included.\n") 9.102 + return nil 9.103 + } 9.104 + verification, ok := data["verification"].(string) 9.105 + if !ok { 9.106 + log.Printf("Ignoring event with non-string verification.\n") 9.107 + return nil 9.108 + } 9.109 + login, err := authClient.GetLogin(event.ID) 9.110 + if err != nil { 9.111 + log.Printf("Error retrieving login %s: %+v", event.ID, err) 9.112 + return err // requeue the message for later processing 9.113 + } 9.114 + login.Verification = verification 9.115 + fmt.Printf("Pretending to email %s their verification code (%s)\n", login.Value, login.Verification) 9.116 + var body bytes.Buffer 9.117 + var htmlBody bytes.Buffer 9.118 + err = emailTextTemplate.Execute(&body, map[string]interface{}{ 9.119 + "login": login, 9.120 + }) 9.121 + if err != nil { 9.122 + log.Printf("Error generating email body: %+v\n", err) 9.123 + return err 9.124 + } 9.125 + err = emailHTMLTemplate.Execute(&htmlBody, map[string]interface{}{ 9.126 + "login": login, 9.127 + }) 9.128 + m := mailgun.NewMessage("Team Ducky <quack@useducky.com>", "Welcome to Ducky! Please verify your address.", string(body.Bytes()), login.Value) 9.129 + m.SetHtml(string(htmlBody.Bytes())) 9.130 + mgmsg, id, err := mgClient.Send(m) 9.131 + if err != nil { 9.132 + log.Printf("Error sending message to mailgun: %+v\n", err) 9.133 + return err 9.134 + } 9.135 + log.Printf("Sent message %s to mailgun: %s", id, mgmsg) 9.136 + return nil 9.137 +}
10.1 --- a/profile.go Sun May 17 02:18:07 2015 -0400 10.2 +++ b/profile.go Sun May 17 02:27:36 2015 -0400 10.3 @@ -39,6 +39,8 @@ 10.4 ErrLoginAlreadyExists = errors.New("login already exists in profileStore") 10.5 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore. 10.6 ErrLoginNotFound = errors.New("login not found in profileStore") 10.7 + // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code. 10.8 + ErrLoginVerificationInvalid = errors.New("login verification code incorrect") 10.9 10.10 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a 10.11 // Passphrase, and requires one. 10.12 @@ -194,11 +196,18 @@ 10.13 // a given Profile that can be used to log into that Profile. 10.14 // Each Profile may only have one Login for each Type. 10.15 type Login struct { 10.16 - Type string `json:"type,omitempty"` 10.17 - Value string `json:"value,omitempty"` 10.18 - ProfileID uuid.ID `json:"profile_id,omitempty"` 10.19 - Created time.Time `json:"created,omitempty"` 10.20 - LastUsed time.Time `json:"last_used,omitempty"` 10.21 + Type string `json:"type,omitempty"` 10.22 + Value string `json:"value,omitempty"` 10.23 + ProfileID uuid.ID `json:"profile_id,omitempty"` 10.24 + Created time.Time `json:"created,omitempty"` 10.25 + LastUsed time.Time `json:"last_used,omitempty"` 10.26 + Verification string `json:"-"` 10.27 + Verified bool `json:"verified"` 10.28 +} 10.29 + 10.30 +type LoginChange struct { 10.31 + Verification *string `json:"verification,omitempty"` 10.32 + ResendVerification *bool `json:"resend_verification,omitempty"` 10.33 } 10.34 10.35 type newProfileRequest struct { 10.36 @@ -207,44 +216,44 @@ 10.37 Name string `json:"name"` 10.38 } 10.39 10.40 -func validateNewProfileRequest(req *newProfileRequest) []requestError { 10.41 - errors := []requestError{} 10.42 +func validateNewProfileRequest(req *newProfileRequest) []RequestError { 10.43 + errors := []RequestError{} 10.44 req.Name = strings.TrimSpace(req.Name) 10.45 req.Email = strings.TrimSpace(req.Email) 10.46 if len(req.Passphrase) < MinPassphraseLength { 10.47 - errors = append(errors, requestError{ 10.48 - Slug: requestErrInsufficient, 10.49 + errors = append(errors, RequestError{ 10.50 + Slug: RequestErrInsufficient, 10.51 Field: "/passphrase", 10.52 }) 10.53 } 10.54 if len(req.Passphrase) > MaxPassphraseLength { 10.55 - errors = append(errors, requestError{ 10.56 - Slug: requestErrOverflow, 10.57 + errors = append(errors, RequestError{ 10.58 + Slug: RequestErrOverflow, 10.59 Field: "/passphrase", 10.60 }) 10.61 } 10.62 if len(req.Name) > MaxNameLength { 10.63 - errors = append(errors, requestError{ 10.64 - Slug: requestErrOverflow, 10.65 + errors = append(errors, RequestError{ 10.66 + Slug: RequestErrOverflow, 10.67 Field: "/name", 10.68 }) 10.69 } 10.70 if req.Email == "" { 10.71 - errors = append(errors, requestError{ 10.72 - Slug: requestErrMissing, 10.73 + errors = append(errors, RequestError{ 10.74 + Slug: RequestErrMissing, 10.75 Field: "/email", 10.76 }) 10.77 } 10.78 if len(req.Email) > MaxEmailLength { 10.79 - errors = append(errors, requestError{ 10.80 - Slug: requestErrOverflow, 10.81 + errors = append(errors, RequestError{ 10.82 + Slug: RequestErrOverflow, 10.83 Field: "/email", 10.84 }) 10.85 } 10.86 re := regexp.MustCompile(".+@.+\\..+") 10.87 if !re.Match([]byte(req.Email)) { 10.88 - errors = append(errors, requestError{ 10.89 - Slug: requestErrInvalidFormat, 10.90 + errors = append(errors, RequestError{ 10.91 + Slug: RequestErrInvalidFormat, 10.92 Field: "/email", 10.93 }) 10.94 } 10.95 @@ -260,9 +269,11 @@ 10.96 deleteProfile(id uuid.ID) error 10.97 10.98 addLogin(login Login) error 10.99 + getLogin(value string) (Login, error) 10.100 removeLogin(value string, profile uuid.ID) error 10.101 removeLoginsByProfile(profile uuid.ID) error 10.102 recordLoginUse(value string, when time.Time) error 10.103 + verifyLogin(value, verification string) error 10.104 listLogins(profile uuid.ID, num, offset int) ([]Login, error) 10.105 } 10.106 10.107 @@ -352,6 +363,16 @@ 10.108 return nil 10.109 } 10.110 10.111 +func (m *memstore) getLogin(value string) (Login, error) { 10.112 + m.loginLock.RLock() 10.113 + defer m.loginLock.RUnlock() 10.114 + l, ok := m.logins[value] 10.115 + if !ok { 10.116 + return Login{}, ErrLoginNotFound 10.117 + } 10.118 + return l, nil 10.119 +} 10.120 + 10.121 func (m *memstore) removeLogin(value string, profile uuid.ID) error { 10.122 m.loginLock.Lock() 10.123 defer m.loginLock.Unlock() 10.124 @@ -402,6 +423,21 @@ 10.125 return nil 10.126 } 10.127 10.128 +func (m *memstore) verifyLogin(value, verification string) error { 10.129 + m.loginLock.Lock() 10.130 + defer m.loginLock.Unlock() 10.131 + l, ok := m.logins[value] 10.132 + if !ok { 10.133 + return ErrLoginNotFound 10.134 + } 10.135 + if l.Verification != verification { 10.136 + return ErrLoginVerificationInvalid 10.137 + } 10.138 + l.Verified = true 10.139 + m.logins[value] = l 10.140 + return nil 10.141 +} 10.142 + 10.143 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) { 10.144 m.loginLock.RLock() 10.145 defer m.loginLock.RUnlock() 10.146 @@ -466,35 +502,37 @@ 10.147 // BUG(paddy): We need to implement a handler that will add a login to a profile. 10.148 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login? 10.149 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile. 10.150 + r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS") 10.151 + r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS") 10.152 } 10.153 10.154 // GetProfileHandler is an HTTP handler for retrieving a profile. 10.155 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { 10.156 - errors := []requestError{} 10.157 + errors := []RequestError{} 10.158 authz := r.Header.Get("Authorization") 10.159 if !strings.HasPrefix(authz, "Bearer ") { 10.160 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.161 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 10.162 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.163 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 10.164 return 10.165 } 10.166 authz = strings.TrimPrefix(authz, "Bearer ") 10.167 vars := mux.Vars(r) 10.168 if vars["id"] == "" { 10.169 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 10.170 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 10.171 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 10.172 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.173 return 10.174 } 10.175 id, err := uuid.Parse(vars["id"]) 10.176 if err != nil { 10.177 - errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) 10.178 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 10.179 + errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) 10.180 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.181 return 10.182 } 10.183 token, err := context.GetToken(authz, false) 10.184 if err != nil || token.Revoked { 10.185 if err == ErrTokenNotFound || token.Revoked { 10.186 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.187 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 10.188 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.189 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 10.190 return 10.191 } else { 10.192 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 10.193 @@ -502,21 +540,21 @@ 10.194 } 10.195 } 10.196 if !id.Equal(token.ProfileID) { 10.197 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.198 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 10.199 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.200 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 10.201 return 10.202 } 10.203 profile, err := context.GetProfileByID(id) 10.204 if err != nil { 10.205 if err == ErrProfileNotFound { 10.206 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 10.207 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 10.208 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) 10.209 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 10.210 return 10.211 } 10.212 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 10.213 return 10.214 } 10.215 - encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}}) 10.216 + encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) 10.217 return 10.218 } 10.219 10.220 @@ -529,7 +567,7 @@ 10.221 return 10.222 } 10.223 var req newProfileRequest 10.224 - errors := []requestError{} 10.225 + errors := []RequestError{} 10.226 decoder := json.NewDecoder(r.Body) 10.227 err := decoder.Decode(&req) 10.228 if err != nil { 10.229 @@ -538,7 +576,7 @@ 10.230 } 10.231 errors = append(errors, validateNewProfileRequest(&req)...) 10.232 if len(errors) > 0 { 10.233 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 10.234 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.235 return 10.236 } 10.237 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations) 10.238 @@ -560,7 +598,7 @@ 10.239 err = context.SaveProfile(profile) 10.240 if err != nil { 10.241 if err == ErrProfileAlreadyExists { 10.242 - encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}}) 10.243 + encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}}) 10.244 return 10.245 } 10.246 log.Printf("Error saving profile: %#+v\n", err) 10.247 @@ -569,16 +607,17 @@ 10.248 } 10.249 logins := []Login{} 10.250 login := Login{ 10.251 - Type: "email", 10.252 - Value: req.Email, 10.253 - Created: profile.Created, 10.254 - LastUsed: profile.Created, 10.255 - ProfileID: profile.ID, 10.256 + Type: "email", 10.257 + Value: req.Email, 10.258 + Created: profile.Created, 10.259 + LastUsed: profile.Created, 10.260 + ProfileID: profile.ID, 10.261 + Verification: uuid.NewID().String(), 10.262 } 10.263 err = context.AddLogin(login) 10.264 if err != nil { 10.265 if err == ErrLoginAlreadyExists { 10.266 - encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}}) 10.267 + encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}}) 10.268 return 10.269 } 10.270 log.Printf("Error adding login: %#+v\n", err) 10.271 @@ -586,48 +625,48 @@ 10.272 return 10.273 } 10.274 logins = append(logins, login) 10.275 - resp := response{ 10.276 + resp := Response{ 10.277 Logins: logins, 10.278 Profiles: []Profile{profile}, 10.279 } 10.280 encode(w, r, http.StatusCreated, resp) 10.281 - // TODO(paddy): should we kick off the email validation flow? 10.282 + go context.SendLoginVerification(login) 10.283 } 10.284 10.285 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { 10.286 - errors := []requestError{} 10.287 + errors := []RequestError{} 10.288 vars := mux.Vars(r) 10.289 if vars["id"] == "" { 10.290 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 10.291 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 10.292 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 10.293 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.294 return 10.295 } 10.296 id, err := uuid.Parse(vars["id"]) 10.297 if err != nil { 10.298 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.299 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 10.300 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.301 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.302 return 10.303 } 10.304 username, password, ok := r.BasicAuth() 10.305 if !ok { 10.306 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.307 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 10.308 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.309 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 10.310 return 10.311 } 10.312 profile, err := authenticate(username, password, context) 10.313 if err != nil { 10.314 if isAuthError(err) { 10.315 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.316 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 10.317 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.318 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 10.319 } else { 10.320 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 10.321 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 10.322 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 10.323 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 10.324 } 10.325 return 10.326 } 10.327 if !profile.ID.Equal(id) { 10.328 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.329 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 10.330 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.331 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 10.332 return 10.333 } 10.334 var req ProfileChange 10.335 @@ -646,13 +685,13 @@ 10.336 req.LastSeen = nil 10.337 if req.Passphrase != nil { 10.338 if len(*req.Passphrase) < MinPassphraseLength { 10.339 - errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/passphrase"}) 10.340 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 10.341 + errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"}) 10.342 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.343 return 10.344 } 10.345 if len(*req.Passphrase) > MaxPassphraseLength { 10.346 - errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/passphrase"}) 10.347 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 10.348 + errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"}) 10.349 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.350 return 10.351 } 10.352 iterations := context.config.iterations 10.353 @@ -679,13 +718,13 @@ 10.354 err = req.Validate() 10.355 if err != nil { 10.356 var status int 10.357 - var resp response 10.358 + var resp Response 10.359 switch err { 10.360 case ErrEmptyChange: 10.361 resp.Profiles = []Profile{profile} 10.362 status = http.StatusOK 10.363 default: 10.364 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 10.365 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 10.366 resp.Errors = errors 10.367 status = http.StatusInternalServerError 10.368 } 10.369 @@ -695,64 +734,145 @@ 10.370 err = context.UpdateProfile(id, req) 10.371 if err != nil { 10.372 if err == ErrProfileNotFound { 10.373 - errors = append(errors, requestError{Slug: requestErrNotFound}) 10.374 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 10.375 + errors = append(errors, RequestError{Slug: RequestErrNotFound}) 10.376 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 10.377 return 10.378 } 10.379 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 10.380 return 10.381 } 10.382 profile.ApplyChange(req) 10.383 - encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}}) 10.384 + encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) 10.385 return 10.386 } 10.387 10.388 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { 10.389 - errors := []requestError{} 10.390 + errors := []RequestError{} 10.391 vars := mux.Vars(r) 10.392 if vars["id"] == "" { 10.393 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 10.394 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 10.395 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 10.396 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.397 return 10.398 } 10.399 id, err := uuid.Parse(vars["id"]) 10.400 if err != nil { 10.401 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.402 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 10.403 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.404 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.405 return 10.406 } 10.407 username, password, ok := r.BasicAuth() 10.408 if !ok { 10.409 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.410 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 10.411 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.412 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 10.413 return 10.414 } 10.415 profile, err := authenticate(username, password, context) 10.416 if err != nil { 10.417 if isAuthError(err) { 10.418 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.419 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 10.420 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.421 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 10.422 } else { 10.423 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 10.424 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 10.425 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 10.426 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 10.427 } 10.428 return 10.429 } 10.430 if !profile.ID.Equal(id) { 10.431 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 10.432 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 10.433 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 10.434 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 10.435 return 10.436 } 10.437 err = context.DeleteProfile(id) 10.438 if err != nil { 10.439 if err == ErrProfileNotFound { 10.440 - errors = append(errors, requestError{Slug: requestErrNotFound}) 10.441 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 10.442 + errors = append(errors, RequestError{Slug: RequestErrNotFound}) 10.443 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 10.444 return 10.445 } 10.446 encode(w, r, http.StatusInternalServerError, actOfGodResponse) 10.447 return 10.448 } 10.449 - encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}}) 10.450 + encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) 10.451 go cleanUpAfterProfileDeletion(profile.ID, context) 10.452 } 10.453 + 10.454 +func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) { 10.455 + var errors []RequestError 10.456 + vars := mux.Vars(r) 10.457 + if vars["login"] == "" { 10.458 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"}) 10.459 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.460 + return 10.461 + } 10.462 + login, err := context.GetLogin(vars["login"]) 10.463 + if err != nil { 10.464 + if err == ErrLoginNotFound { 10.465 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) 10.466 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 10.467 + return 10.468 + } 10.469 + log.Printf("Error retrieving login: %#+v\n", err) 10.470 + encode(w, r, http.StatusInternalServerError, actOfGodResponse) 10.471 + return 10.472 + } 10.473 + encode(w, r, http.StatusOK, Response{Logins: []Login{login}}) 10.474 +} 10.475 + 10.476 +func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) { 10.477 + var errors []RequestError 10.478 + vars := mux.Vars(r) 10.479 + if vars["login"] == "" { 10.480 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"}) 10.481 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.482 + return 10.483 + } 10.484 + var req LoginChange 10.485 + decoder := json.NewDecoder(r.Body) 10.486 + err := decoder.Decode(&req) 10.487 + if err != nil { 10.488 + log.Printf("Error decoding request: %#+v\n", err) 10.489 + encode(w, r, http.StatusBadRequest, invalidFormatResponse) 10.490 + return 10.491 + } 10.492 + login, err := context.GetLogin(vars["login"]) 10.493 + if err != nil { 10.494 + if err == ErrLoginNotFound { 10.495 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) 10.496 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 10.497 + return 10.498 + } 10.499 + log.Printf("Error retrieving login: %#+v\n", err) 10.500 + encode(w, r, http.StatusInternalServerError, actOfGodResponse) 10.501 + return 10.502 + } 10.503 + if req.Verification != nil { 10.504 + err = context.VerifyLogin(vars["login"], *req.Verification) 10.505 + if err != nil { 10.506 + if err == ErrLoginNotFound { 10.507 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) 10.508 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 10.509 + return 10.510 + } else if err == ErrLoginVerificationInvalid { 10.511 + errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"}) 10.512 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.513 + return 10.514 + } 10.515 + log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err) 10.516 + encode(w, r, http.StatusInternalServerError, actOfGodResponse) 10.517 + return 10.518 + } 10.519 + login.Verified = true 10.520 + } else if req.ResendVerification != nil { 10.521 + if !*req.ResendVerification { 10.522 + errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"}) 10.523 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.524 + return 10.525 + } 10.526 + context.SendLoginVerification(login) 10.527 + } else { 10.528 + errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"}) 10.529 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 10.530 + return 10.531 + } 10.532 + encode(w, r, http.StatusOK, Response{Logins: []Login{login}}) 10.533 +}
11.1 --- a/profile_postgres.go Sun May 17 02:18:07 2015 -0400 11.2 +++ b/profile_postgres.go Sun May 17 02:27:36 2015 -0400 11.3 @@ -259,6 +259,66 @@ 11.4 return err 11.5 } 11.6 11.7 +func (p *postgres) verifyLoginSQL(value, verification string) *pan.Query { 11.8 + var login Login 11.9 + query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(login)+" SET ") 11.10 + query.Include(pan.GetUnquotedColumn(login, "Verified")+" = ?", true) 11.11 + query.IncludeWhere() 11.12 + query.FlushExpressions(" ") 11.13 + query.Include(pan.GetUnquotedColumn(login, "Value")+" = ?", value) 11.14 + query.Include(pan.GetUnquotedColumn(login, "Verification")+" = ?", verification) 11.15 + return query.FlushExpressions(" AND ") 11.16 +} 11.17 + 11.18 +func (p *postgres) verifyLogin(value, verification string) error { 11.19 + query := p.verifyLoginSQL(value, verification) 11.20 + res, err := p.db.Exec(query.String(), query.Args...) 11.21 + if err != nil { 11.22 + return err 11.23 + } 11.24 + rows, err := res.RowsAffected() 11.25 + if err != nil { 11.26 + return err 11.27 + } 11.28 + if rows == 0 { 11.29 + return ErrLoginVerificationInvalid 11.30 + } 11.31 + return nil 11.32 +} 11.33 + 11.34 +func (p *postgres) getLoginSQL(value string) *pan.Query { 11.35 + var login Login 11.36 + fields, _ := pan.GetFields(login) 11.37 + query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(login)) 11.38 + query.IncludeWhere() 11.39 + query.Include(pan.GetUnquotedColumn(login, "Value")+" = ?", value) 11.40 + return query.FlushExpressions(" ") 11.41 +} 11.42 + 11.43 +func (p *postgres) getLogin(value string) (Login, error) { 11.44 + query := p.getLoginSQL(value) 11.45 + rows, err := p.db.Query(query.String(), query.Args...) 11.46 + if err != nil { 11.47 + return Login{}, err 11.48 + } 11.49 + var login Login 11.50 + var found bool 11.51 + for rows.Next() { 11.52 + err := pan.Unmarshal(rows, &login) 11.53 + if err != nil { 11.54 + return Login{}, err 11.55 + } 11.56 + found = true 11.57 + } 11.58 + if err := rows.Err(); err != nil { 11.59 + return Login{}, err 11.60 + } 11.61 + if !found { 11.62 + return login, ErrLoginNotFound 11.63 + } 11.64 + return login, nil 11.65 +} 11.66 + 11.67 func (p *postgres) listLoginsSQL(profile uuid.ID, num, offset int) *pan.Query { 11.68 var login Login 11.69 fields, _ := pan.GetFields(login)
12.1 --- a/profile_test.go Sun May 17 02:18:07 2015 -0400 12.2 +++ b/profile_test.go Sun May 17 02:27:36 2015 -0400 12.3 @@ -90,6 +90,12 @@ 12.4 if !login1.LastUsed.Equal(login2.LastUsed) { 12.5 return false, "LastUsed", login1.LastUsed, login2.LastUsed 12.6 } 12.7 + if login1.Verification != login2.Verification { 12.8 + return false, "Verification", login1.Verification, login2.Verification 12.9 + } 12.10 + if login1.Verified != login2.Verified { 12.11 + return false, "Verified", login1.Verified, login2.Verified 12.12 + } 12.13 return true, "", nil, nil 12.14 } 12.15
13.1 --- a/request.go Sun May 17 02:18:07 2015 -0400 13.2 +++ b/request.go Sun May 17 02:27:36 2015 -0400 13.3 @@ -9,26 +9,26 @@ 13.4 ) 13.5 13.6 const ( 13.7 - requestErrAccessDenied = "access_denied" 13.8 - requestErrInsufficient = "insufficient" 13.9 - requestErrOverflow = "overflow" 13.10 - requestErrInvalidValue = "invalid_value" 13.11 - requestErrInvalidFormat = "invalid_format" 13.12 - requestErrMissing = "missing" 13.13 - requestErrNotFound = "not_found" 13.14 - requestErrConflict = "conflict" 13.15 - requestErrActOfGod = "act_of_god" 13.16 + RequestErrAccessDenied = "access_denied" 13.17 + RequestErrInsufficient = "insufficient" 13.18 + RequestErrOverflow = "overflow" 13.19 + RequestErrInvalidValue = "invalid_value" 13.20 + RequestErrInvalidFormat = "invalid_format" 13.21 + RequestErrMissing = "missing" 13.22 + RequestErrNotFound = "not_found" 13.23 + RequestErrConflict = "conflict" 13.24 + RequestErrActOfGod = "act_of_god" 13.25 ) 13.26 13.27 var ( 13.28 - actOfGodResponse = response{Errors: []requestError{requestError{Slug: requestErrActOfGod}}} 13.29 - invalidFormatResponse = response{Errors: []requestError{requestError{Slug: requestErrInvalidFormat, Field: "/"}}} 13.30 + actOfGodResponse = Response{Errors: []RequestError{RequestError{Slug: RequestErrActOfGod}}} 13.31 + invalidFormatResponse = Response{Errors: []RequestError{RequestError{Slug: RequestErrInvalidFormat, Field: "/"}}} 13.32 13.33 encoders = []string{"application/json"} 13.34 ) 13.35 13.36 -type response struct { 13.37 - Errors []requestError `json:"errors,omitempty"` 13.38 +type Response struct { 13.39 + Errors []RequestError `json:"errors,omitempty"` 13.40 Logins []Login `json:"logins,omitempty"` 13.41 Profiles []Profile `json:"profiles,omitempty"` 13.42 Clients []Client `json:"clients,omitempty"` 13.43 @@ -36,7 +36,7 @@ 13.44 Sessions []Session `json:"sessions,omitempty"` 13.45 } 13.46 13.47 -type requestError struct { 13.48 +type RequestError struct { 13.49 Slug string `json:"error,omitempty"` 13.50 Field string `json:"field,omitempty"` 13.51 Param string `json:"param,omitempty"` 13.52 @@ -57,7 +57,7 @@ 13.53 }) 13.54 } 13.55 13.56 -func encode(w http.ResponseWriter, r *http.Request, status int, resp response) { 13.57 +func encode(w http.ResponseWriter, r *http.Request, status int, resp Response) { 13.58 contentType := goautoneg.Negotiate(r.Header.Get("Accept"), encoders) 13.59 w.Header().Set("content-type", contentType) 13.60 w.WriteHeader(status)
14.1 --- a/request_test.go Sun May 17 02:18:07 2015 -0400 14.2 +++ b/request_test.go Sun May 17 02:27:36 2015 -0400 14.3 @@ -2,7 +2,7 @@ 14.4 14.5 import "fmt" 14.6 14.7 -func compareErrors(err1, err2 requestError) (success bool, field string, val1, val2 interface{}) { 14.8 +func compareErrors(err1, err2 RequestError) (success bool, field string, val1, val2 interface{}) { 14.9 if err1.Slug != err2.Slug { 14.10 return false, "Slug", err1.Slug, err2.Slug 14.11 } 14.12 @@ -18,7 +18,7 @@ 14.13 return true, "", nil, nil 14.14 } 14.15 14.16 -func compareResponses(resp1, resp2 response) (success bool, field string, val1, val2 interface{}) { 14.17 +func compareResponses(resp1, resp2 Response) (success bool, field string, val1, val2 interface{}) { 14.18 if len(resp1.Errors) != len(resp2.Errors) { 14.19 return false, "Errors", resp1.Errors, resp2.Errors 14.20 } 14.21 @@ -72,7 +72,7 @@ 14.22 return true, "", nil, nil 14.23 } 14.24 14.25 -func fillInServerGenerated(expectation, result response) { 14.26 +func fillInServerGenerated(expectation, result Response) { 14.27 if len(expectation.Profiles) > 0 { 14.28 for pos, profile := range expectation.Profiles { 14.29 profile.ID = result.Profiles[pos].ID
15.1 --- a/session.go Sun May 17 02:18:07 2015 -0400 15.2 +++ b/session.go Sun May 17 02:27:36 2015 -0400 15.3 @@ -357,60 +357,60 @@ 15.4 15.5 // TerminateSessionHandler allows the user to end their session before it expires. 15.6 func TerminateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { 15.7 - var errors []requestError 15.8 + var errors []RequestError 15.9 vars := mux.Vars(r) 15.10 if vars["id"] == "" { 15.11 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 15.12 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 15.13 + errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) 15.14 + encode(w, r, http.StatusBadRequest, Response{Errors: errors}) 15.15 return 15.16 } 15.17 id := vars["id"] 15.18 un, pw, ok := r.BasicAuth() 15.19 if !ok { 15.20 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 15.21 - encode(w, r, http.StatusUnauthorized, response{Errors: errors}) 15.22 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 15.23 + encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) 15.24 return 15.25 } 15.26 profile, err := authenticate(un, pw, context) 15.27 if err != nil { 15.28 if isAuthError(err) { 15.29 - errors = append(errors, requestError{Slug: requestErrAccessDenied}) 15.30 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 15.31 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) 15.32 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 15.33 return 15.34 } 15.35 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 15.36 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 15.37 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 15.38 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 15.39 return 15.40 } 15.41 session, err := context.GetSession(id) 15.42 if err != nil { 15.43 if err == ErrSessionNotFound { 15.44 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 15.45 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 15.46 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) 15.47 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 15.48 return 15.49 } 15.50 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 15.51 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 15.52 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 15.53 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 15.54 return 15.55 } 15.56 if !session.ProfileID.Equal(profile.ID) { 15.57 - errors = append(errors, requestError{Slug: requestErrAccessDenied, Param: "id"}) 15.58 - encode(w, r, http.StatusForbidden, response{Errors: errors}) 15.59 + errors = append(errors, RequestError{Slug: RequestErrAccessDenied, Param: "id"}) 15.60 + encode(w, r, http.StatusForbidden, Response{Errors: errors}) 15.61 return 15.62 } 15.63 err = context.TerminateSession(id) 15.64 if err != nil { 15.65 if err == ErrSessionNotFound { 15.66 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 15.67 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 15.68 + errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) 15.69 + encode(w, r, http.StatusNotFound, Response{Errors: errors}) 15.70 return 15.71 } 15.72 - errors = append(errors, requestError{Slug: requestErrActOfGod}) 15.73 - encode(w, r, http.StatusInternalServerError, response{Errors: errors}) 15.74 + errors = append(errors, RequestError{Slug: RequestErrActOfGod}) 15.75 + encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) 15.76 return 15.77 } 15.78 session.Active = false 15.79 - encode(w, r, http.StatusOK, response{Sessions: []Session{session}, Errors: errors}) 15.80 + encode(w, r, http.StatusOK, Response{Sessions: []Session{session}, Errors: errors}) 15.81 } 15.82 15.83 func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) {
16.1 --- a/sql/postgres_init.sql Sun May 17 02:18:07 2015 -0400 16.2 +++ b/sql/postgres_init.sql Sun May 17 02:27:36 2015 -0400 16.3 @@ -18,7 +18,9 @@ 16.4 value VARCHAR(64) PRIMARY KEY, 16.5 profile_id VARCHAR(36) NOT NULL, 16.6 created TIMESTAMPTZ NOT NULL, 16.7 - last_used TIMESTAMPTZ NOT NULL 16.8 + last_used TIMESTAMPTZ NOT NULL, 16.9 + verification VARCHAR(36) NOT NULL, 16.10 + verified BOOLEAN NOT NULL 16.11 ); 16.12 16.13 CREATE TABLE IF NOT EXISTS clients (