add support for resend an invite email
This change solves the User's API problem when you want to create an user that its email hasn't been verified yet but it exist. At now, you can resend invitation email using endpoint /users/{id}/resend-invitation Fixes #184
This commit is contained in:
parent
f51125f555
commit
8156870862
8 changed files with 595 additions and 13 deletions
|
@ -45,8 +45,9 @@ var (
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
User: user.User{
|
User: user.User{
|
||||||
ID: "ID-2",
|
ID: "ID-2",
|
||||||
Email: "Email-2@example.com",
|
Email: "Email-2@example.com",
|
||||||
|
EmailVerified: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -582,6 +583,184 @@ func TestDisableUser(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResendEmailInvitation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
req schema.ResendEmailInvitationRequest
|
||||||
|
cantEmail bool
|
||||||
|
userID string
|
||||||
|
email string
|
||||||
|
token string
|
||||||
|
|
||||||
|
wantResponse schema.ResendEmailInvitationResponse
|
||||||
|
wantCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
|
||||||
|
req: schema.ResendEmailInvitationRequest{
|
||||||
|
RedirectURL: testRedirectURL.String(),
|
||||||
|
},
|
||||||
|
|
||||||
|
userID: "ID-3",
|
||||||
|
email: "Email-3@example.com",
|
||||||
|
token: userGoodToken,
|
||||||
|
|
||||||
|
wantResponse: schema.ResendEmailInvitationResponse{
|
||||||
|
EmailSent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
req: schema.ResendEmailInvitationRequest{
|
||||||
|
RedirectURL: testRedirectURL.String(),
|
||||||
|
},
|
||||||
|
|
||||||
|
userID: "ID-3",
|
||||||
|
email: "Email-3@example.com",
|
||||||
|
cantEmail: true,
|
||||||
|
token: userGoodToken,
|
||||||
|
|
||||||
|
wantResponse: schema.ResendEmailInvitationResponse{
|
||||||
|
ResetPasswordLink: testResetPasswordURL.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
req: schema.ResendEmailInvitationRequest{
|
||||||
|
RedirectURL: "http://scammers.com",
|
||||||
|
},
|
||||||
|
|
||||||
|
userID: "ID-3",
|
||||||
|
email: "Email-3@example.com",
|
||||||
|
token: userGoodToken,
|
||||||
|
|
||||||
|
wantCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
req: schema.ResendEmailInvitationRequest{
|
||||||
|
RedirectURL: testRedirectURL.String(),
|
||||||
|
},
|
||||||
|
|
||||||
|
userID: "ID-2",
|
||||||
|
email: "Email-2@example.com",
|
||||||
|
token: userGoodToken,
|
||||||
|
|
||||||
|
wantCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
req: schema.ResendEmailInvitationRequest{
|
||||||
|
RedirectURL: testRedirectURL.String(),
|
||||||
|
},
|
||||||
|
|
||||||
|
userID: "ID-3",
|
||||||
|
email: "Email-3@example.com",
|
||||||
|
token: userBadTokenClientNotAdmin,
|
||||||
|
|
||||||
|
wantCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
req: schema.ResendEmailInvitationRequest{
|
||||||
|
RedirectURL: testRedirectURL.String(),
|
||||||
|
},
|
||||||
|
|
||||||
|
userID: "ID-3",
|
||||||
|
email: "Email-3@example.com",
|
||||||
|
token: userBadClientID,
|
||||||
|
|
||||||
|
wantCode: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
req: schema.ResendEmailInvitationRequest{
|
||||||
|
RedirectURL: testRedirectURL.String(),
|
||||||
|
},
|
||||||
|
|
||||||
|
userID: "ID-3",
|
||||||
|
email: "Email-3@example.com",
|
||||||
|
token: userBadTokenExpired,
|
||||||
|
|
||||||
|
wantCode: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
req: schema.ResendEmailInvitationRequest{
|
||||||
|
RedirectURL: testRedirectURL.String(),
|
||||||
|
},
|
||||||
|
|
||||||
|
userID: "ID-3",
|
||||||
|
email: "Email-3@example.com",
|
||||||
|
token: userBadTokenDisabled,
|
||||||
|
|
||||||
|
wantCode: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
req: schema.ResendEmailInvitationRequest{
|
||||||
|
RedirectURL: testRedirectURL.String(),
|
||||||
|
},
|
||||||
|
|
||||||
|
userID: "ID-3",
|
||||||
|
email: "Email-3@example.com",
|
||||||
|
token: userBadTokenNotAdmin,
|
||||||
|
|
||||||
|
wantCode: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
func() {
|
||||||
|
f := makeUserAPITestFixtures()
|
||||||
|
defer f.close()
|
||||||
|
f.trans.Token = tt.token
|
||||||
|
f.emailer.cantEmail = tt.cantEmail
|
||||||
|
|
||||||
|
page, err := f.client.Users.ResendEmailInvitation(tt.userID, &tt.req).Do()
|
||||||
|
if tt.wantCode != 0 {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("case %d: err was nil", i)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gErr, ok := err.(*googleapi.Error)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("case %d: not a googleapi Error: %q", i, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if gErr.Code != tt.wantCode {
|
||||||
|
t.Errorf("case %d: want=%d, got=%d", i, tt.wantCode, gErr.Code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("case %d: want nil err, got: %v %T ", i, err, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := pretty.Compare(tt.wantResponse, page); diff != "" {
|
||||||
|
t.Errorf("case %d: Compare(want, got) = %v", i, diff)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
urlParsed, err := url.Parse(tt.req.RedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("case %d unexpected err: %v", i, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wantEmalier := testEmailer{
|
||||||
|
cantEmail: tt.cantEmail,
|
||||||
|
lastEmail: tt.email,
|
||||||
|
lastClientID: "XXX",
|
||||||
|
lastWasInvite: true,
|
||||||
|
lastRedirectURL: *urlParsed,
|
||||||
|
}
|
||||||
|
if diff := pretty.Compare(wantEmalier, f.emailer); diff != "" {
|
||||||
|
t.Errorf("case %d: Compare(want, got) = %v", i, diff)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type testEmailer struct {
|
type testEmailer struct {
|
||||||
cantEmail bool
|
cantEmail bool
|
||||||
lastEmail string
|
lastEmail string
|
||||||
|
|
|
@ -59,6 +59,27 @@ __Version:__ v1
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ResendEmailInvitationRequest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
redirectURL: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ResendEmailInvitationResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
emailSent: boolean,
|
||||||
|
resetPasswordLink: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### User
|
### User
|
||||||
|
|
||||||
|
|
||||||
|
@ -303,3 +324,30 @@ __Version:__ v1
|
||||||
| default | Unexpected error | |
|
| default | Unexpected error | |
|
||||||
|
|
||||||
|
|
||||||
|
### POST /users/{id}/resend-invitation
|
||||||
|
|
||||||
|
> __Summary__
|
||||||
|
|
||||||
|
> ResendEmailInvitation Users
|
||||||
|
|
||||||
|
> __Description__
|
||||||
|
|
||||||
|
> Resend invitation email to an existing user with unverified email.
|
||||||
|
|
||||||
|
|
||||||
|
> __Parameters__
|
||||||
|
|
||||||
|
> |Name|Located in|Description|Required|Type|
|
||||||
|
|:-----|:-----|:-----|:-----|:-----|
|
||||||
|
| id | path | | Yes | string |
|
||||||
|
| | body | | Yes | [ResendEmailInvitationRequest](#resendemailinvitationrequest) |
|
||||||
|
|
||||||
|
|
||||||
|
> __Responses__
|
||||||
|
|
||||||
|
> |Code|Description|Type|
|
||||||
|
|:-----|:-----|:-----|
|
||||||
|
| 200 | | [ResendEmailInvitationResponse](#resendemailinvitationresponse) |
|
||||||
|
| default | Unexpected error | |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"google.golang.org/api/googleapi"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Always reference these packages, just in case the auto-generated code
|
// Always reference these packages, just in case the auto-generated code
|
||||||
|
@ -103,6 +102,16 @@ type Error struct {
|
||||||
Error_description string `json:"error_description,omitempty"`
|
Error_description string `json:"error_description,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResendEmailInvitationRequest struct {
|
||||||
|
RedirectURL string `json:"redirectURL,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResendEmailInvitationResponse struct {
|
||||||
|
EmailSent bool `json:"emailSent,omitempty"`
|
||||||
|
|
||||||
|
ResetPasswordLink string `json:"resetPasswordLink,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Admin bool `json:"admin,omitempty"`
|
Admin bool `json:"admin,omitempty"`
|
||||||
|
|
||||||
|
@ -607,3 +616,87 @@ func (c *UsersListCall) Do() (*UsersResponse, error) {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// method id "dex.User.ResendEmailInvitation":
|
||||||
|
|
||||||
|
type UsersResendEmailInvitationCall struct {
|
||||||
|
s *Service
|
||||||
|
id string
|
||||||
|
resendemailinvitationrequest *ResendEmailInvitationRequest
|
||||||
|
opt_ map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResendEmailInvitation: Resend invitation email to an existing user
|
||||||
|
// with unverified email.
|
||||||
|
func (r *UsersService) ResendEmailInvitation(id string, resendemailinvitationrequest *ResendEmailInvitationRequest) *UsersResendEmailInvitationCall {
|
||||||
|
c := &UsersResendEmailInvitationCall{s: r.s, opt_: make(map[string]interface{})}
|
||||||
|
c.id = id
|
||||||
|
c.resendemailinvitationrequest = resendemailinvitationrequest
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields allows partial responses to be retrieved.
|
||||||
|
// See https://developers.google.com/gdata/docs/2.0/basics#PartialResponse
|
||||||
|
// for more information.
|
||||||
|
func (c *UsersResendEmailInvitationCall) Fields(s ...googleapi.Field) *UsersResendEmailInvitationCall {
|
||||||
|
c.opt_["fields"] = googleapi.CombineFields(s)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UsersResendEmailInvitationCall) Do() (*ResendEmailInvitationResponse, error) {
|
||||||
|
var body io.Reader = nil
|
||||||
|
body, err := googleapi.WithoutDataWrapper.JSONReader(c.resendemailinvitationrequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctype := "application/json"
|
||||||
|
params := make(url.Values)
|
||||||
|
params.Set("alt", "json")
|
||||||
|
if v, ok := c.opt_["fields"]; ok {
|
||||||
|
params.Set("fields", fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
urls := googleapi.ResolveRelative(c.s.BasePath, "users/{id}/resend-invitation")
|
||||||
|
urls += "?" + params.Encode()
|
||||||
|
req, _ := http.NewRequest("POST", urls, body)
|
||||||
|
googleapi.Expand(req.URL, map[string]string{
|
||||||
|
"id": c.id,
|
||||||
|
})
|
||||||
|
req.Header.Set("Content-Type", ctype)
|
||||||
|
req.Header.Set("User-Agent", "google-api-go-client/0.5")
|
||||||
|
res, err := c.s.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer googleapi.CloseBody(res)
|
||||||
|
if err := googleapi.CheckResponse(res); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var ret *ResendEmailInvitationResponse
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&ret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
// {
|
||||||
|
// "description": "Resend invitation email to an existing user with unverified email.",
|
||||||
|
// "httpMethod": "POST",
|
||||||
|
// "id": "dex.User.ResendEmailInvitation",
|
||||||
|
// "parameterOrder": [
|
||||||
|
// "id"
|
||||||
|
// ],
|
||||||
|
// "parameters": {
|
||||||
|
// "id": {
|
||||||
|
// "location": "path",
|
||||||
|
// "required": true,
|
||||||
|
// "type": "string"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "path": "users/{id}/resend-invitation",
|
||||||
|
// "request": {
|
||||||
|
// "$ref": "ResendEmailInvitationRequest"
|
||||||
|
// },
|
||||||
|
// "response": {
|
||||||
|
// "$ref": "ResendEmailInvitationResponse"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -188,6 +188,28 @@ const DiscoveryJSON = `{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ResendEmailInvitationRequest": {
|
||||||
|
"id": "UserDisableRequest",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"redirectURL": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ResendEmailInvitationResponse": {
|
||||||
|
"id": "UserDisableResponse",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"resetPasswordLink": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"emailSent": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
|
@ -295,6 +317,28 @@ const DiscoveryJSON = `{
|
||||||
"response": {
|
"response": {
|
||||||
"$ref": "UserDisableResponse"
|
"$ref": "UserDisableResponse"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ResendEmailInvitation": {
|
||||||
|
"id": "dex.User.ResendEmailInvitation",
|
||||||
|
"description": "Resend invitation email to an existing user with unverified email.",
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "users/{id}/resend-invitation",
|
||||||
|
"parameters": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"location": "path"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameterOrder": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"$ref": "ResendEmailInvitationRequest"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"$ref": "ResendEmailInvitationResponse"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,6 +182,28 @@
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ResendEmailInvitationRequest": {
|
||||||
|
"id": "UserDisableRequest",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"redirectURL": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ResendEmailInvitationResponse": {
|
||||||
|
"id": "UserDisableResponse",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"resetPasswordLink": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"emailSent": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
|
@ -289,6 +311,28 @@
|
||||||
"response": {
|
"response": {
|
||||||
"$ref": "UserDisableResponse"
|
"$ref": "UserDisableResponse"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ResendEmailInvitation": {
|
||||||
|
"id": "dex.User.ResendEmailInvitation",
|
||||||
|
"description": "Resend invitation email to an existing user with unverified email.",
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "users/{id}/resend-invitation",
|
||||||
|
"parameters": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"location": "path"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameterOrder": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"$ref": "ResendEmailInvitationRequest"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"$ref": "ResendEmailInvitationResponse"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,12 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
UsersSubTree = "/users"
|
UsersSubTree = "/users"
|
||||||
UsersListEndpoint = addBasePath(UsersSubTree)
|
UsersListEndpoint = addBasePath(UsersSubTree)
|
||||||
UsersCreateEndpoint = addBasePath(UsersSubTree)
|
UsersCreateEndpoint = addBasePath(UsersSubTree)
|
||||||
UsersGetEndpoint = addBasePath(UsersSubTree + "/:id")
|
UsersGetEndpoint = addBasePath(UsersSubTree + "/:id")
|
||||||
UsersDisableEndpoint = addBasePath(UsersSubTree + "/:id/disable")
|
UsersDisableEndpoint = addBasePath(UsersSubTree + "/:id/disable")
|
||||||
|
UsersResendInvitationEndpoint = addBasePath(UsersSubTree + "/:id/resend-invitation")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserMgmtServer struct {
|
type UserMgmtServer struct {
|
||||||
|
@ -55,6 +56,7 @@ func (s *UserMgmtServer) HTTPHandler() http.Handler {
|
||||||
r.POST(UsersCreateEndpoint, s.authAPIHandle(s.createUser))
|
r.POST(UsersCreateEndpoint, s.authAPIHandle(s.createUser))
|
||||||
r.POST(UsersDisableEndpoint, s.authAPIHandle(s.disableUser))
|
r.POST(UsersDisableEndpoint, s.authAPIHandle(s.disableUser))
|
||||||
r.GET(UsersGetEndpoint, s.authAPIHandle(s.getUser))
|
r.GET(UsersGetEndpoint, s.authAPIHandle(s.getUser))
|
||||||
|
r.POST(UsersResendInvitationEndpoint, s.authAPIHandle(s.resendInvitationEmail))
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,6 +163,34 @@ func (s *UserMgmtServer) disableUser(w http.ResponseWriter, r *http.Request, ps
|
||||||
writeResponseWithBody(w, http.StatusOK, resp)
|
writeResponseWithBody(w, http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserMgmtServer) resendInvitationEmail(w http.ResponseWriter, r *http.Request, ps httprouter.Params, creds api.Creds) {
|
||||||
|
id := ps.ByName("id")
|
||||||
|
if id == "" {
|
||||||
|
writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidRequest, "id is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resendEmailInvitationReq := schema.ResendEmailInvitationRequest{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&resendEmailInvitationReq); err != nil {
|
||||||
|
writeInvalidRequest(w, "cannot parse JSON body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redirURL, err := url.Parse(resendEmailInvitationReq.RedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
writeAPIError(w, http.StatusBadRequest,
|
||||||
|
newAPIError(errorInvalidRequest, "redirectURL must be a valid URL"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resendEmailInvitationResponse, err := s.api.ResendEmailInvitation(creds, id, *redirURL)
|
||||||
|
if err != nil {
|
||||||
|
s.writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeResponseWithBody(w, http.StatusOK, resendEmailInvitationResponse)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *UserMgmtServer) writeError(w http.ResponseWriter, err error) {
|
func (s *UserMgmtServer) writeError(w http.ResponseWriter, err error) {
|
||||||
log.Errorf("Error calling user management API: %v: ", err)
|
log.Errorf("Error calling user management API: %v: ", err)
|
||||||
if apiErr, ok := err.(api.Error); ok {
|
if apiErr, ok := err.(api.Error); ok {
|
||||||
|
|
|
@ -24,7 +24,8 @@ var (
|
||||||
client.ErrorNotFound: ErrorInvalidClient,
|
client.ErrorNotFound: ErrorInvalidClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorInvalidEmail = newError("invalid_email", "invalid email.", http.StatusBadRequest)
|
ErrorInvalidEmail = newError("invalid_email", "invalid email.", http.StatusBadRequest)
|
||||||
|
ErrorVerifiedEmail = newError("verified_email", "Email already verified.", http.StatusBadRequest)
|
||||||
|
|
||||||
ErrorInvalidClient = newError("invalid_client", "invalid email.", http.StatusBadRequest)
|
ErrorInvalidClient = newError("invalid_client", "invalid email.", http.StatusBadRequest)
|
||||||
|
|
||||||
|
@ -188,6 +189,50 @@ func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (s
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UsersAPI) ResendEmailInvitation(creds Creds, userID string, redirURL url.URL) (schema.ResendEmailInvitationResponse, error) {
|
||||||
|
log.Infof("userAPI: ResendEmailInvitation")
|
||||||
|
if !u.Authorize(creds) {
|
||||||
|
return schema.ResendEmailInvitationResponse{}, ErrorUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := u.clientIdentityRepo.Metadata(creds.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return schema.ResendEmailInvitationResponse{}, mapError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
validRedirURL, err := client.ValidRedirectURL(&redirURL, metadata.RedirectURIs)
|
||||||
|
if err != nil {
|
||||||
|
return schema.ResendEmailInvitationResponse{}, ErrorInvalidRedirectURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve user to check if it's already created
|
||||||
|
userUser, err := u.manager.Get(userID)
|
||||||
|
if err != nil {
|
||||||
|
return schema.ResendEmailInvitationResponse{}, mapError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email is verified
|
||||||
|
if userUser.EmailVerified {
|
||||||
|
return schema.ResendEmailInvitationResponse{}, ErrorVerifiedEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := u.emailer.SendInviteEmail(userUser.Email, validRedirURL, creds.ClientID)
|
||||||
|
|
||||||
|
// An email is sent only if we don't get a link and there's no error.
|
||||||
|
emailSent := err == nil && url == nil
|
||||||
|
|
||||||
|
// If email is not sent a reset link will be generated
|
||||||
|
var resetLink string
|
||||||
|
if url != nil {
|
||||||
|
resetLink = url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.ResendEmailInvitationResponse{
|
||||||
|
EmailSent: emailSent,
|
||||||
|
ResetPasswordLink: resetLink,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UsersAPI) ListUsers(creds Creds, maxResults int, nextPageToken string) ([]*schema.User, string, error) {
|
func (u *UsersAPI) ListUsers(creds Creds, maxResults int, nextPageToken string) ([]*schema.User, string, error) {
|
||||||
log.Infof("userAPI: ListUsers")
|
log.Infof("userAPI: ListUsers")
|
||||||
|
|
||||||
|
|
|
@ -99,9 +99,10 @@ func makeTestFixtures() (*UsersAPI, *testEmailer) {
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
User: user.User{
|
User: user.User{
|
||||||
ID: "ID-2",
|
ID: "ID-2",
|
||||||
Email: "id2@example.com",
|
Email: "id2@example.com",
|
||||||
CreatedAt: clock.Now(),
|
EmailVerified: true,
|
||||||
|
CreatedAt: clock.Now(),
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
User: user.User{
|
User: user.User{
|
||||||
|
@ -463,3 +464,101 @@ func TestDisableUsers(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func TestResendEmailInvitation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
creds Creds
|
||||||
|
userID string
|
||||||
|
email string
|
||||||
|
redirURL url.URL
|
||||||
|
cantEmail bool
|
||||||
|
|
||||||
|
wantResponse schema.ResendEmailInvitationResponse
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
creds: goodCreds,
|
||||||
|
userID: "ID-1",
|
||||||
|
email: "id1@example.com",
|
||||||
|
redirURL: validRedirURL,
|
||||||
|
|
||||||
|
wantResponse: schema.ResendEmailInvitationResponse{
|
||||||
|
EmailSent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
creds: goodCreds,
|
||||||
|
userID: "ID-1",
|
||||||
|
email: "id1@example.com",
|
||||||
|
redirURL: validRedirURL,
|
||||||
|
cantEmail: true,
|
||||||
|
|
||||||
|
wantResponse: schema.ResendEmailInvitationResponse{
|
||||||
|
EmailSent: false,
|
||||||
|
ResetPasswordLink: resetPasswordURL.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
creds: badCreds,
|
||||||
|
userID: "ID-1",
|
||||||
|
email: "id1@example.com",
|
||||||
|
redirURL: validRedirURL,
|
||||||
|
|
||||||
|
wantErr: ErrorUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
creds: goodCreds,
|
||||||
|
userID: "ID-1",
|
||||||
|
email: "id1@example.com",
|
||||||
|
redirURL: url.URL{Host: "scammers.com"},
|
||||||
|
|
||||||
|
wantErr: ErrorInvalidRedirectURL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
creds: goodCreds,
|
||||||
|
userID: "ID-2",
|
||||||
|
email: "id2@example.com",
|
||||||
|
redirURL: validRedirURL,
|
||||||
|
|
||||||
|
wantErr: ErrorVerifiedEmail,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
creds: goodCreds,
|
||||||
|
userID: "non-existent",
|
||||||
|
email: "non-existent@example.com",
|
||||||
|
redirURL: validRedirURL,
|
||||||
|
|
||||||
|
wantErr: ErrorResourceNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
api, emailer := makeTestFixtures()
|
||||||
|
emailer.cantEmail = tt.cantEmail
|
||||||
|
|
||||||
|
response, err := api.ResendEmailInvitation(tt.creds, tt.userID, tt.redirURL)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
if err != tt.wantErr {
|
||||||
|
t.Errorf("case %d: want=%q, got=%q", i, tt.wantErr, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("case %d: want nil err, got: %q ", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := pretty.Compare(tt.wantResponse, response); diff != "" {
|
||||||
|
t.Errorf("case %d: Compare(want, got) = %v", i, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantEmailer := testEmailer{
|
||||||
|
cantEmail: tt.cantEmail,
|
||||||
|
lastEmail: tt.email,
|
||||||
|
lastClientID: tt.creds.ClientID,
|
||||||
|
lastRedirectURL: tt.redirURL,
|
||||||
|
lastWasInvite: true,
|
||||||
|
}
|
||||||
|
if diff := pretty.Compare(wantEmailer, emailer); diff != "" {
|
||||||
|
t.Errorf("case %d: Compare(want, got) = %v", i, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Reference in a new issue