Merge pull request #331 from Tecsisa/184-resend-email-invitation
add support for resend an invite email
This commit is contained in:
commit
c92aae647c
8 changed files with 595 additions and 13 deletions
|
@ -47,6 +47,7 @@ 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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ var (
|
||||||
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 {
|
||||||
|
|
|
@ -25,6 +25,7 @@ var (
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
|
|
|
@ -101,6 +101,7 @@ func makeTestFixtures() (*UsersAPI, *testEmailer) {
|
||||||
User: user.User{
|
User: user.User{
|
||||||
ID: "ID-2",
|
ID: "ID-2",
|
||||||
Email: "id2@example.com",
|
Email: "id2@example.com",
|
||||||
|
EmailVerified: true,
|
||||||
CreatedAt: clock.Now(),
|
CreatedAt: clock.Now(),
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
@ -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