auth

Paddy 2015-05-17 Parent:cf1aef6eb81f Child:b7e685839a1b

172:8ecb60d29b0d Go to Latest

auth/client.go

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.

History
     1.1 --- a/client.go	Sun May 17 02:18:07 2015 -0400
     1.2 +++ b/client.go	Sun May 17 02:27:36 2015 -0400
     1.3 @@ -455,22 +455,22 @@
     1.4  }
     1.5  
     1.6  func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
     1.7 -	errors := []requestError{}
     1.8 +	errors := []RequestError{}
     1.9  	username, password, ok := r.BasicAuth()
    1.10  	if !ok {
    1.11 -		errors = append(errors, requestError{Slug: requestErrAccessDenied})
    1.12 -		encode(w, r, http.StatusUnauthorized, response{Errors: errors})
    1.13 +		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
    1.14 +		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
    1.15  		return
    1.16  	}
    1.17  	profile, err := authenticate(username, password, c)
    1.18  	if err != nil {
    1.19  		if isAuthError(err) {
    1.20 -			errors = append(errors, requestError{Slug: requestErrAccessDenied})
    1.21 -			encode(w, r, http.StatusUnauthorized, response{Errors: errors})
    1.22 +			errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
    1.23 +			encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
    1.24  		} else {
    1.25  			log.Printf("Error authenticating: %#+v\n", err)
    1.26 -			errors = append(errors, requestError{Slug: requestErrActOfGod})
    1.27 -			encode(w, r, http.StatusInternalServerError, response{Errors: errors})
    1.28 +			errors = append(errors, RequestError{Slug: RequestErrActOfGod})
    1.29 +			encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
    1.30  		}
    1.31  		return
    1.32  	}
    1.33 @@ -482,19 +482,19 @@
    1.34  		return
    1.35  	}
    1.36  	if req.Type == "" {
    1.37 -		errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
    1.38 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/type"})
    1.39  	} else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
    1.40 -		errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
    1.41 +		errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/type"})
    1.42  	}
    1.43  	if req.Name == "" {
    1.44 -		errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
    1.45 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/name"})
    1.46  	} else if len(req.Name) < minClientNameLen {
    1.47 -		errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
    1.48 +		errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"})
    1.49  	} else if len(req.Name) > maxClientNameLen {
    1.50 -		errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
    1.51 +		errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"})
    1.52  	}
    1.53  	if len(errors) > 0 {
    1.54 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
    1.55 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
    1.56  		return
    1.57  	}
    1.58  	client := Client{
    1.59 @@ -518,8 +518,8 @@
    1.60  	err = c.SaveClient(client)
    1.61  	if err != nil {
    1.62  		if err == ErrClientAlreadyExists {
    1.63 -			errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
    1.64 -			encode(w, r, http.StatusBadRequest, response{Errors: errors})
    1.65 +			errors = append(errors, RequestError{Slug: RequestErrConflict, Field: "/id"})
    1.66 +			encode(w, r, http.StatusBadRequest, Response{Errors: errors})
    1.67  			return
    1.68  		}
    1.69  		log.Printf("Error saving client: %#+v\n", err)
    1.70 @@ -530,11 +530,11 @@
    1.71  	for pos, u := range req.Endpoints {
    1.72  		uri, err := url.Parse(u)
    1.73  		if err != nil {
    1.74 -			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
    1.75 +			errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
    1.76  			continue
    1.77  		}
    1.78  		if !uri.IsAbs() {
    1.79 -			errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
    1.80 +			errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
    1.81  			continue
    1.82  		}
    1.83  		endpoint := Endpoint{
    1.84 @@ -548,11 +548,11 @@
    1.85  	err = c.AddEndpoints(endpoints)
    1.86  	if err != nil {
    1.87  		log.Printf("Error adding endpoints: %#+v\n", err)
    1.88 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
    1.89 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
    1.90 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
    1.91 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors, Clients: []Client{client}})
    1.92  		return
    1.93  	}
    1.94 -	resp := response{
    1.95 +	resp := Response{
    1.96  		Clients:   []Client{client},
    1.97  		Endpoints: endpoints,
    1.98  		Errors:    errors,
    1.99 @@ -561,28 +561,28 @@
   1.100  }
   1.101  
   1.102  func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
   1.103 -	errors := []requestError{}
   1.104 +	errors := []RequestError{}
   1.105  	vars := mux.Vars(r)
   1.106  	if vars["id"] == "" {
   1.107 -		errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
   1.108 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.109 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
   1.110 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.111  		return
   1.112  	}
   1.113  	id, err := uuid.Parse(vars["id"])
   1.114  	if err != nil {
   1.115 -		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
   1.116 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.117 +		errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
   1.118 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.119  		return
   1.120  	}
   1.121  	client, err := c.GetClient(id)
   1.122  	if err != nil {
   1.123  		if err == ErrClientNotFound {
   1.124 -			errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
   1.125 -			encode(w, r, http.StatusNotFound, response{Errors: errors})
   1.126 +			errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
   1.127 +			encode(w, r, http.StatusNotFound, Response{Errors: errors})
   1.128  			return
   1.129  		}
   1.130 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.131 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.132 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.133 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.134  		return
   1.135  	}
   1.136  	username, password, ok := r.BasicAuth()
   1.137 @@ -592,11 +592,11 @@
   1.138  		profile, err := authenticate(username, password, c)
   1.139  		if err != nil {
   1.140  			if isAuthError(err) {
   1.141 -				errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.142 -				encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.143 +				errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.144 +				encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.145  			} else {
   1.146 -				errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.147 -				encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.148 +				errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.149 +				encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.150  			}
   1.151  			return
   1.152  		}
   1.153 @@ -604,7 +604,7 @@
   1.154  			client.Secret = ""
   1.155  		}
   1.156  	}
   1.157 -	resp := response{
   1.158 +	resp := Response{
   1.159  		Clients: []Client{client},
   1.160  		Errors:  errors,
   1.161  	}
   1.162 @@ -612,7 +612,7 @@
   1.163  }
   1.164  
   1.165  func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
   1.166 -	errors := []requestError{}
   1.167 +	errors := []RequestError{}
   1.168  	var err error
   1.169  	// BUG(paddy): If ids are provided in query params, retrieve only those clients
   1.170  	num := defaultClientResponseSize
   1.171 @@ -623,38 +623,38 @@
   1.172  	if numStr != "" {
   1.173  		num, err = strconv.Atoi(numStr)
   1.174  		if err != nil {
   1.175 -			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
   1.176 +			errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"})
   1.177  		}
   1.178  		if num > maxClientResponseSize {
   1.179 -			errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
   1.180 +			errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"})
   1.181  		}
   1.182  		if num < 1 {
   1.183 -			errors = append(errors, requestError{Slug: requestErrInsufficient, Param: "num"})
   1.184 +			errors = append(errors, RequestError{Slug: RequestErrInsufficient, Param: "num"})
   1.185  		}
   1.186  	}
   1.187  	if offsetStr != "" {
   1.188  		offset, err = strconv.Atoi(offsetStr)
   1.189  		if err != nil {
   1.190 -			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
   1.191 +			errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"})
   1.192  		}
   1.193  	}
   1.194  	if ownerIDStr == "" {
   1.195 -		errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
   1.196 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "owner_id"})
   1.197  	}
   1.198  	if len(errors) > 0 {
   1.199 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.200 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.201  		return
   1.202  	}
   1.203  	ownerID, err := uuid.Parse(ownerIDStr)
   1.204  	if err != nil {
   1.205 -		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
   1.206 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.207 +		errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "owner_id"})
   1.208 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.209  		return
   1.210  	}
   1.211  	clients, err := c.ListClientsByOwner(ownerID, num, offset)
   1.212  	if err != nil {
   1.213 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.214 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.215 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.216 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.217  		return
   1.218  	}
   1.219  	username, password, ok := r.BasicAuth()
   1.220 @@ -667,11 +667,11 @@
   1.221  		profile, err := authenticate(username, password, c)
   1.222  		if err != nil {
   1.223  			if isAuthError(err) {
   1.224 -				errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.225 -				encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.226 +				errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.227 +				encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.228  			} else {
   1.229 -				errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.230 -				encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.231 +				errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.232 +				encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.233  			}
   1.234  			return
   1.235  		}
   1.236 @@ -682,7 +682,7 @@
   1.237  			}
   1.238  		}
   1.239  	}
   1.240 -	resp := response{
   1.241 +	resp := Response{
   1.242  		Clients: clients,
   1.243  		Errors:  errors,
   1.244  	}
   1.245 @@ -690,80 +690,80 @@
   1.246  }
   1.247  
   1.248  func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
   1.249 -	errors := []requestError{}
   1.250 +	errors := []RequestError{}
   1.251  	vars := mux.Vars(r)
   1.252  	if _, ok := vars["id"]; !ok {
   1.253 -		errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
   1.254 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.255 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
   1.256 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.257  		return
   1.258  	}
   1.259  	id, err := uuid.Parse(vars["id"])
   1.260  	if err != nil {
   1.261 -		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
   1.262 +		errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
   1.263  	}
   1.264  	username, password, ok := r.BasicAuth()
   1.265  	if !ok {
   1.266 -		errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.267 -		encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.268 +		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.269 +		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.270  		return
   1.271  	}
   1.272  	profile, err := authenticate(username, password, c)
   1.273  	if err != nil {
   1.274  		if isAuthError(err) {
   1.275 -			errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.276 -			encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.277 +			errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.278 +			encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.279  		} else {
   1.280 -			errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.281 -			encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.282 +			errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.283 +			encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.284  		}
   1.285  		return
   1.286  	}
   1.287  	var change ClientChange
   1.288  	err = decode(r, &change)
   1.289  	if err != nil {
   1.290 -		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
   1.291 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.292 +		errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/"})
   1.293 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.294  		return
   1.295  	}
   1.296  	errs := change.Validate()
   1.297  	for _, err := range errs {
   1.298  		switch err {
   1.299  		case ErrEmptyChange:
   1.300 -			errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
   1.301 +			errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
   1.302  		case ErrClientNameTooShort:
   1.303 -			errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
   1.304 +			errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"})
   1.305  		case ErrClientNameTooLong:
   1.306 -			errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
   1.307 +			errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"})
   1.308  		case ErrClientLogoTooLong:
   1.309 -			errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
   1.310 +			errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/logo"})
   1.311  		case ErrClientLogoNotURL:
   1.312 -			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
   1.313 +			errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/logo"})
   1.314  		case ErrClientWebsiteTooLong:
   1.315 -			errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
   1.316 +			errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/website"})
   1.317  		case ErrClientWebsiteNotURL:
   1.318 -			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
   1.319 +			errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/website"})
   1.320  		default:
   1.321  			log.Println("Unrecognised error from client change validation:", err)
   1.322  		}
   1.323  	}
   1.324  	if len(errors) > 0 {
   1.325 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.326 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.327  		return
   1.328  	}
   1.329  	client, err := c.GetClient(id)
   1.330  	if err == ErrClientNotFound {
   1.331 -		errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
   1.332 -		encode(w, r, http.StatusNotFound, response{Errors: errors})
   1.333 +		errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
   1.334 +		encode(w, r, http.StatusNotFound, Response{Errors: errors})
   1.335  		return
   1.336  	} else if err != nil {
   1.337  		log.Println("Error retrieving client:", err)
   1.338 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.339 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.340 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.341 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.342  		return
   1.343  	}
   1.344  	if !client.OwnerID.Equal(profile.ID) {
   1.345 -		errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.346 -		encode(w, r, http.StatusForbidden, response{Errors: errors})
   1.347 +		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.348 +		encode(w, r, http.StatusForbidden, Response{Errors: errors})
   1.349  		return
   1.350  	}
   1.351  	if change.Secret != nil && client.Type == clientTypeConfidential {
   1.352 @@ -779,59 +779,59 @@
   1.353  	err = c.UpdateClient(id, change)
   1.354  	if err != nil {
   1.355  		log.Println("Error updating client:", err)
   1.356 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.357 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.358 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.359 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.360  		return
   1.361  	}
   1.362  	client.ApplyChange(change)
   1.363 -	encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
   1.364 +	encode(w, r, http.StatusOK, Response{Clients: []Client{client}, Errors: errors})
   1.365  	return
   1.366  }
   1.367  
   1.368  func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
   1.369 -	errors := []requestError{}
   1.370 +	errors := []RequestError{}
   1.371  	vars := mux.Vars(r)
   1.372  	if _, ok := vars["id"]; !ok {
   1.373 -		errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
   1.374 -		encode(w, r, http.StatusNotFound, response{Errors: errors})
   1.375 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
   1.376 +		encode(w, r, http.StatusNotFound, Response{Errors: errors})
   1.377  		return
   1.378  	}
   1.379  	id, err := uuid.Parse(vars["id"])
   1.380  	if err != nil {
   1.381 -		errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
   1.382 +		errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
   1.383  	}
   1.384  	username, password, ok := r.BasicAuth()
   1.385  	if !ok {
   1.386 -		errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.387 -		encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.388 +		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.389 +		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.390  		return
   1.391  	}
   1.392  	profile, err := authenticate(username, password, c)
   1.393  	if err != nil {
   1.394  		if isAuthError(err) {
   1.395 -			errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.396 -			encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.397 +			errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.398 +			encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.399  		} else {
   1.400 -			errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.401 -			encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.402 +			errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.403 +			encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.404  		}
   1.405  		return
   1.406  	}
   1.407  	client, err := c.GetClient(id)
   1.408  	if err != nil {
   1.409  		if err == ErrClientNotFound {
   1.410 -			errors = append(errors, requestError{Slug: requestErrNotFound})
   1.411 -			encode(w, r, http.StatusNotFound, response{Errors: errors})
   1.412 +			errors = append(errors, RequestError{Slug: RequestErrNotFound})
   1.413 +			encode(w, r, http.StatusNotFound, Response{Errors: errors})
   1.414  			return
   1.415  		}
   1.416  		log.Println("Error retrieving client:", err)
   1.417 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.418 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.419 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.420 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.421  		return
   1.422  	}
   1.423  	if !client.OwnerID.Equal(profile.ID) {
   1.424 -		errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.425 -		encode(w, r, http.StatusForbidden, response{Errors: errors})
   1.426 +		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.427 +		encode(w, r, http.StatusForbidden, Response{Errors: errors})
   1.428  		return
   1.429  	}
   1.430  	deleted := true
   1.431 @@ -839,16 +839,16 @@
   1.432  	err = c.UpdateClient(id, change)
   1.433  	if err != nil {
   1.434  		if err == ErrClientNotFound {
   1.435 -			errors = append(errors, requestError{Slug: requestErrNotFound})
   1.436 -			encode(w, r, http.StatusNotFound, response{Errors: errors})
   1.437 +			errors = append(errors, RequestError{Slug: RequestErrNotFound})
   1.438 +			encode(w, r, http.StatusNotFound, Response{Errors: errors})
   1.439  			return
   1.440  		}
   1.441  		log.Println("Error deleting client:", err)
   1.442 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.443 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.444 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.445 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.446  		return
   1.447  	}
   1.448 -	encode(w, r, http.StatusOK, response{Errors: errors})
   1.449 +	encode(w, r, http.StatusOK, Response{Errors: errors})
   1.450  	go cleanUpAfterClientDeletion(id, c)
   1.451  	return
   1.452  }
   1.453 @@ -857,50 +857,50 @@
   1.454  	type addEndpointReq struct {
   1.455  		Endpoints []string `json:"endpoints"`
   1.456  	}
   1.457 -	errors := []requestError{}
   1.458 +	errors := []RequestError{}
   1.459  	vars := mux.Vars(r)
   1.460  	if vars["id"] == "" {
   1.461 -		errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
   1.462 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.463 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
   1.464 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.465  		return
   1.466  	}
   1.467  	id, err := uuid.Parse(vars["id"])
   1.468  	if err != nil {
   1.469 -		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
   1.470 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.471 +		errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
   1.472 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.473  		return
   1.474  	}
   1.475  	username, password, ok := r.BasicAuth()
   1.476  	if !ok {
   1.477 -		errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.478 -		encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.479 +		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.480 +		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.481  		return
   1.482  	}
   1.483  	profile, err := authenticate(username, password, c)
   1.484  	if err != nil {
   1.485  		if isAuthError(err) {
   1.486 -			errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.487 -			encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.488 +			errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.489 +			encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.490  		} else {
   1.491 -			errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.492 -			encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.493 +			errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.494 +			encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.495  		}
   1.496  		return
   1.497  	}
   1.498  	client, err := c.GetClient(id)
   1.499  	if err != nil {
   1.500  		if err == ErrClientNotFound {
   1.501 -			errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
   1.502 -			encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.503 +			errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
   1.504 +			encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.505  			return
   1.506  		}
   1.507 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.508 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.509 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.510 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.511  		return
   1.512  	}
   1.513  	if !client.OwnerID.Equal(profile.ID) {
   1.514 -		errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.515 -		encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.516 +		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.517 +		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.518  		return
   1.519  	}
   1.520  	var req addEndpointReq
   1.521 @@ -911,17 +911,17 @@
   1.522  		return
   1.523  	}
   1.524  	if len(req.Endpoints) < 1 {
   1.525 -		errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
   1.526 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.527 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/endpoints"})
   1.528 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.529  		return
   1.530  	}
   1.531  	endpoints := []Endpoint{}
   1.532  	for pos, u := range req.Endpoints {
   1.533  		if parsed, err := url.Parse(u); err != nil {
   1.534 -			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
   1.535 +			errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
   1.536  			continue
   1.537  		} else if !parsed.IsAbs() {
   1.538 -			errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
   1.539 +			errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
   1.540  			continue
   1.541  		}
   1.542  		e := Endpoint{
   1.543 @@ -933,7 +933,7 @@
   1.544  		endpoints = append(endpoints, e)
   1.545  	}
   1.546  	if len(errors) > 0 {
   1.547 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.548 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.549  		return
   1.550  	}
   1.551  	err = c.AddEndpoints(endpoints)
   1.552 @@ -941,7 +941,7 @@
   1.553  		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
   1.554  		return
   1.555  	}
   1.556 -	resp := response{
   1.557 +	resp := Response{
   1.558  		Errors:    errors,
   1.559  		Endpoints: endpoints,
   1.560  	}
   1.561 @@ -949,12 +949,12 @@
   1.562  }
   1.563  
   1.564  func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
   1.565 -	errors := []requestError{}
   1.566 +	errors := []RequestError{}
   1.567  	vars := mux.Vars(r)
   1.568  	clientID, err := uuid.Parse(vars["id"])
   1.569  	if err != nil {
   1.570 -		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
   1.571 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.572 +		errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"})
   1.573 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.574  		return
   1.575  	}
   1.576  	num := defaultEndpointResponseSize
   1.577 @@ -964,29 +964,29 @@
   1.578  	if numStr != "" {
   1.579  		num, err = strconv.Atoi(numStr)
   1.580  		if err != nil {
   1.581 -			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
   1.582 +			errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"})
   1.583  		}
   1.584  		if num > maxEndpointResponseSize {
   1.585 -			errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
   1.586 +			errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"})
   1.587  		}
   1.588  	}
   1.589  	if offsetStr != "" {
   1.590  		offset, err = strconv.Atoi(offsetStr)
   1.591  		if err != nil {
   1.592 -			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
   1.593 +			errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"})
   1.594  		}
   1.595  	}
   1.596  	if len(errors) > 0 {
   1.597 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.598 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.599  		return
   1.600  	}
   1.601  	endpoints, err := c.ListEndpoints(clientID, num, offset)
   1.602  	if err != nil {
   1.603 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.604 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.605 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.606 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.607  		return
   1.608  	}
   1.609 -	resp := response{
   1.610 +	resp := Response{
   1.611  		Endpoints: endpoints,
   1.612  		Errors:    errors,
   1.613  	}
   1.614 @@ -994,72 +994,72 @@
   1.615  }
   1.616  
   1.617  func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
   1.618 -	errors := []requestError{}
   1.619 +	errors := []RequestError{}
   1.620  	vars := mux.Vars(r)
   1.621  	if vars["client_id"] == "" {
   1.622 -		errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"})
   1.623 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.624 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "client_id"})
   1.625 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.626  		return
   1.627  	}
   1.628  	clientID, err := uuid.Parse(vars["client_id"])
   1.629  	if err != nil {
   1.630 -		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
   1.631 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.632 +		errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"})
   1.633 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.634  		return
   1.635  	}
   1.636  	if vars["id"] == "" {
   1.637 -		errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
   1.638 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.639 +		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
   1.640 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.641  		return
   1.642  	}
   1.643  	id, err := uuid.Parse(vars["id"])
   1.644  	if err != nil {
   1.645 -		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
   1.646 -		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.647 +		errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
   1.648 +		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.649  		return
   1.650  	}
   1.651  	username, password, ok := r.BasicAuth()
   1.652  	if !ok {
   1.653 -		errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.654 -		encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.655 +		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.656 +		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.657  		return
   1.658  	}
   1.659  	profile, err := authenticate(username, password, c)
   1.660  	if err != nil {
   1.661  		if isAuthError(err) {
   1.662 -			errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.663 -			encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.664 +			errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.665 +			encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.666  		} else {
   1.667 -			errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.668 -			encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.669 +			errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.670 +			encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.671  		}
   1.672  		return
   1.673  	}
   1.674  	client, err := c.GetClient(clientID)
   1.675  	if err != nil {
   1.676  		if err == ErrClientNotFound {
   1.677 -			errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"})
   1.678 -			encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.679 +			errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "client_id"})
   1.680 +			encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.681  			return
   1.682  		}
   1.683 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.684 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.685 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.686 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.687  		return
   1.688  	}
   1.689  	if !client.OwnerID.Equal(profile.ID) {
   1.690 -		errors = append(errors, requestError{Slug: requestErrAccessDenied})
   1.691 -		encode(w, r, http.StatusUnauthorized, response{Errors: errors})
   1.692 +		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
   1.693 +		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
   1.694  		return
   1.695  	}
   1.696  	endpoint, err := c.GetEndpoint(clientID, id)
   1.697  	if err != nil {
   1.698  		if err == ErrEndpointNotFound {
   1.699 -			errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
   1.700 -			encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.701 +			errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
   1.702 +			encode(w, r, http.StatusBadRequest, Response{Errors: errors})
   1.703  			return
   1.704  		}
   1.705 -		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.706 -		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.707 +		errors = append(errors, RequestError{Slug: RequestErrActOfGod})
   1.708 +		encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
   1.709  		return
   1.710  	}
   1.711  	err = c.RemoveEndpoint(clientID, id)
   1.712 @@ -1067,7 +1067,7 @@
   1.713  		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
   1.714  		return
   1.715  	}
   1.716 -	resp := response{
   1.717 +	resp := Response{
   1.718  		Errors:    errors,
   1.719  		Endpoints: []Endpoint{endpoint},
   1.720  	}