From c220423090a32dd68562a847da179b4233bd5758 Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Wed, 16 Aug 2023 14:54:59 -0400 Subject: [PATCH 01/11] update url for email invitation --- routers/web/auth/auth.go | 5 +++++ services/mailer/mail_team_invite.go | 9 +++++++++ templates/mail/team_invite.tmpl | 9 +++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 3bf133f56222c..7ba7083a877e4 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -398,6 +398,11 @@ func SignUp(ctx *context.Context) { // Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration + redirectTo := ctx.FormString("redirect_to") + if len(redirectTo) > 0 { + middleware.SetRedirectToCookie(ctx.Resp, redirectTo) + } + ctx.HTML(http.StatusOK, tplSignUp) } diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index b6f47ee9215bc..8272eef38731b 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -33,6 +33,13 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod locale := translation.NewLocale(inviter.Language) + // check if a user with this email already exists + user, err := user_model.GetUserByEmail(ctx, invite.Email) + if err != nil && !user_model.IsErrUserNotExist(err) { + return err + } + userAccountExists := err == nil && user != nil + subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName()) mailMeta := map[string]any{ "Inviter": inviter, @@ -40,6 +47,8 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod "Team": team, "Invite": invite, "Subject": subject, + // user to determine whether to link to sign-up or login + "UserAccountExists": userAccountExists, // helper "locale": locale, "Str2html": templates.Str2html, diff --git a/templates/mail/team_invite.tmpl b/templates/mail/team_invite.tmpl index 8357895265393..120fcb438809a 100644 --- a/templates/mail/team_invite.tmpl +++ b/templates/mail/team_invite.tmpl @@ -4,10 +4,15 @@ -{{$invite_url := printf "%sorg/invite/%s" AppUrl (QueryEscape .Invite.Token)}}

{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName) | Str2html}}

-

{{.locale.Tr "mail.team_invite.text_2"}}

{{$invite_url}}

+ {{if .UserAccountExists}} + {{$login_url := printf "%suser/login?redirect_to=%s" AppUrl (QueryEscape (printf "/org/invite/%s" .Invite.Token))}} +

{{.locale.Tr "mail.team_invite.text_2"}}

{{$login_url}}

+ {{else}} + {{$signup_url := printf "%suser/sign_up?redirect_to=%s" AppUrl (QueryEscape (printf "/org/invite/%s" .Invite.Token))}} +

{{.locale.Tr "mail.team_invite.text_2"}}

{{$signup_url}}

+ {{end}}

{{.locale.Tr "mail.link_not_working_do_paste"}}

{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}

From 35c0deb3fbda8dd128816d65e22135ff258ad58f Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Wed, 23 Aug 2023 11:16:40 -0400 Subject: [PATCH 02/11] return error for user with prohibited login --- services/mailer/mail_team_invite.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index 8272eef38731b..f58b5a3a4e328 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -6,6 +6,7 @@ package mailer import ( "bytes" "context" + "fmt" org_model "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" @@ -37,7 +38,10 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod user, err := user_model.GetUserByEmail(ctx, invite.Email) if err != nil && !user_model.IsErrUserNotExist(err) { return err + } else if user.ProhibitLogin { + return fmt.Errorf("login is prohibited for the invited user") } + userAccountExists := err == nil && user != nil subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName()) From 4aa72a12c98575bd6ab0ee05f52287cbbaad05c3 Mon Sep 17 00:00:00 2001 From: Jack Hay Date: Wed, 23 Aug 2023 11:57:02 -0400 Subject: [PATCH 03/11] Check that user is not nil before ProhibitLogin Co-authored-by: Jonathan Tran --- services/mailer/mail_team_invite.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index f58b5a3a4e328..fdbe314c1063a 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -38,7 +38,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod user, err := user_model.GetUserByEmail(ctx, invite.Email) if err != nil && !user_model.IsErrUserNotExist(err) { return err - } else if user.ProhibitLogin { + } else if user != nil && user.ProhibitLogin { return fmt.Errorf("login is prohibited for the invited user") } From 61bf1e76d68efc81d9bfc20398d2fbcd0be3399e Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Thu, 24 Aug 2023 12:10:06 -0400 Subject: [PATCH 04/11] account activation should obey the redirect cookie --- routers/web/auth/auth.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 7ba7083a877e4..c20a45ebc9721 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -734,6 +734,12 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) { } ctx.Flash.Success(ctx.Tr("auth.account_activated")) + if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 { + middleware.DeleteRedirectToCookie(ctx.Resp) + ctx.RedirectToFirst(redirectTo) + return + } + ctx.Redirect(setting.AppSubURL + "/") } From 27d8590f2a2e36448ee5f3cf2cd254952b29a1f9 Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Thu, 24 Aug 2023 13:27:48 -0400 Subject: [PATCH 05/11] signup -> login redirect should use redirect_to if available --- services/auth/middleware.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/auth/middleware.go b/services/auth/middleware.go index d1955a4c90010..330201180db1e 100644 --- a/services/auth/middleware.go +++ b/services/auth/middleware.go @@ -122,7 +122,7 @@ func VerifyAuthWithOptions(options *VerifyOptions) func(ctx *context.Context) { // Redirect to dashboard if user tries to visit any non-login page. if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" { - ctx.Redirect(setting.AppSubURL + "/") + ctx.RedirectToFirst(ctx.FormString("redirect_to")) return } From 079febe083931009bb06f71ec7d4734184721ce8 Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Thu, 24 Aug 2023 15:57:17 -0400 Subject: [PATCH 06/11] update comment --- services/auth/middleware.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/auth/middleware.go b/services/auth/middleware.go index 330201180db1e..4a0b613fa662f 100644 --- a/services/auth/middleware.go +++ b/services/auth/middleware.go @@ -120,7 +120,7 @@ func VerifyAuthWithOptions(options *VerifyOptions) func(ctx *context.Context) { } } - // Redirect to dashboard if user tries to visit any non-login page. + // Redirect to dashboard (or alternate location) if user tries to visit any non-login page. if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" { ctx.RedirectToFirst(ctx.FormString("redirect_to")) return From f6be26e0144726135a680d0c8240a9063e743c7f Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Wed, 30 Aug 2023 11:26:55 -0400 Subject: [PATCH 07/11] add integration tests for login and sign up redirects --- tests/integration/org_team_invite_test.go | 163 ++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go index 4d848dfc6034e..0e047b9867fee 100644 --- a/tests/integration/org_team_invite_test.go +++ b/tests/integration/org_team_invite_test.go @@ -6,6 +6,8 @@ package integration import ( "fmt" "net/http" + "net/url" + "strings" "testing" "code.gitea.io/gitea/models/db" @@ -69,3 +71,164 @@ func TestOrgTeamEmailInvite(t *testing.T) { assert.NoError(t, err) assert.True(t, isMember) } + +func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { + if setting.MailService == nil { + t.Skip() + return + } + + defer tests.PrepareTestEnv(t)() + + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + + isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID) + assert.NoError(t, err) + assert.False(t, isMember) + + // create the invite + session := loginUser(t, "user1") + + teamUrl := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) + csrf := GetCSRF(t, session, teamUrl) + req := NewRequestWithValues(t, "POST", teamUrl+"/action/add", map[string]string{ + "_csrf": csrf, + "uid": "1", + "uname": user.Email, + }) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + // get the invite token + invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID) + assert.NoError(t, err) + assert.Len(t, invites, 1) + + // accept the invite + inviteUrl := fmt.Sprintf("/org/invite/%s", invites[0].Token) + req = NewRequest(t, "GET", fmt.Sprintf("/user/login?redirect_to=%s", url.QueryEscape(inviteUrl))) + resp = MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{ + "_csrf": doc.GetCSRF(), + "user_name": "user5", + "password": "password", + }) + for _, c := range resp.Result().Cookies() { + req.AddCookie(c) + } + + resp = MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, inviteUrl, test.RedirectURL(resp)) + + // complete the login process + ch := http.Header{} + ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";")) + cr := http.Request{Header: ch} + + session = emptyTestSession(t) + baseURL, err := url.Parse(setting.AppURL) + assert.NoError(t, err) + session.jar.SetCookies(baseURL, cr.Cookies()) + + // make the request + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + csrf = GetCSRF(t, session, test.RedirectURL(resp)) + req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ + "_csrf": csrf, + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID) + assert.NoError(t, err) + assert.True(t, isMember) +} + +func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { + if setting.MailService == nil { + t.Skip() + return + } + + defer tests.PrepareTestEnv(t)() + + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + + // create the invite + session := loginUser(t, "user1") + + teamUrl := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) + csrf := GetCSRF(t, session, teamUrl) + req := NewRequestWithValues(t, "POST", teamUrl+"/action/add", map[string]string{ + "_csrf": csrf, + "uid": "1", + "uname": "doesnotexist@example.com", + }) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + // get the invite token + invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID) + assert.NoError(t, err) + assert.Len(t, invites, 1) + + // accept the invite + inviteUrl := fmt.Sprintf("/org/invite/%s", invites[0].Token) + req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteUrl))) + resp = MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + req = NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ + "_csrf": doc.GetCSRF(), + "user_name": "doesnotexist", + "email": "doesnotexist@example.com", + "password": "examplePassword!1", + "retype": "examplePassword!1", + }) + for _, c := range resp.Result().Cookies() { + req.AddCookie(c) + } + + resp = MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, inviteUrl, test.RedirectURL(resp)) + + // complete the signup process + ch := http.Header{} + ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";")) + cr := http.Request{Header: ch} + + session = emptyTestSession(t) + baseURL, err := url.Parse(setting.AppURL) + assert.NoError(t, err) + session.jar.SetCookies(baseURL, cr.Cookies()) + + // make the redirected request + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + csrf = GetCSRF(t, session, test.RedirectURL(resp)) + req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ + "_csrf": csrf, + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + // get the new user + newUser, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist") + assert.NoError(t, err) + + isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, newUser.ID) + assert.NoError(t, err) + assert.True(t, isMember) +} From a3f1deee47160d93f9bb3523bf0a47eb161f58b7 Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Wed, 30 Aug 2023 11:39:55 -0400 Subject: [PATCH 08/11] linting fix --- tests/integration/org_team_invite_test.go | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go index 0e047b9867fee..5f206abce9987 100644 --- a/tests/integration/org_team_invite_test.go +++ b/tests/integration/org_team_invite_test.go @@ -91,9 +91,9 @@ func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { // create the invite session := loginUser(t, "user1") - teamUrl := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) - csrf := GetCSRF(t, session, teamUrl) - req := NewRequestWithValues(t, "POST", teamUrl+"/action/add", map[string]string{ + teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) + csrf := GetCSRF(t, session, teamURL) + req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ "_csrf": csrf, "uid": "1", "uname": user.Email, @@ -108,8 +108,8 @@ func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { assert.Len(t, invites, 1) // accept the invite - inviteUrl := fmt.Sprintf("/org/invite/%s", invites[0].Token) - req = NewRequest(t, "GET", fmt.Sprintf("/user/login?redirect_to=%s", url.QueryEscape(inviteUrl))) + inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token) + req = NewRequest(t, "GET", fmt.Sprintf("/user/login?redirect_to=%s", url.QueryEscape(inviteURL))) resp = MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) @@ -123,7 +123,7 @@ func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { } resp = MakeRequest(t, req, http.StatusSeeOther) - assert.Equal(t, inviteUrl, test.RedirectURL(resp)) + assert.Equal(t, inviteURL, test.RedirectURL(resp)) // complete the login process ch := http.Header{} @@ -166,9 +166,9 @@ func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { // create the invite session := loginUser(t, "user1") - teamUrl := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) - csrf := GetCSRF(t, session, teamUrl) - req := NewRequestWithValues(t, "POST", teamUrl+"/action/add", map[string]string{ + teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) + csrf := GetCSRF(t, session, teamURL) + req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ "_csrf": csrf, "uid": "1", "uname": "doesnotexist@example.com", @@ -183,8 +183,8 @@ func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { assert.Len(t, invites, 1) // accept the invite - inviteUrl := fmt.Sprintf("/org/invite/%s", invites[0].Token) - req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteUrl))) + inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token) + req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL))) resp = MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) @@ -200,7 +200,7 @@ func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { } resp = MakeRequest(t, req, http.StatusSeeOther) - assert.Equal(t, inviteUrl, test.RedirectURL(resp)) + assert.Equal(t, inviteURL, test.RedirectURL(resp)) // complete the signup process ch := http.Header{} From 472ba2f9d3d51f3ed40530e6694f471f497f8cfd Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Wed, 30 Aug 2023 14:54:35 -0400 Subject: [PATCH 09/11] add more integration tests for invites --- tests/integration/org_team_invite_test.go | 177 +++++++++++++++++++--- 1 file changed, 160 insertions(+), 17 deletions(-) diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go index 5f206abce9987..ae46c80ec3dca 100644 --- a/tests/integration/org_team_invite_test.go +++ b/tests/integration/org_team_invite_test.go @@ -39,9 +39,9 @@ func TestOrgTeamEmailInvite(t *testing.T) { session := loginUser(t, "user1") - url := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) - csrf := GetCSRF(t, session, url) - req := NewRequestWithValues(t, "POST", url+"/action/add", map[string]string{ + teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) + csrf := GetCSRF(t, session, teamURL) + req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ "_csrf": csrf, "uid": "1", "uname": user.Email, @@ -58,9 +58,9 @@ func TestOrgTeamEmailInvite(t *testing.T) { session = loginUser(t, user.Name) // join the team - url = fmt.Sprintf("/org/invite/%s", invites[0].Token) - csrf = GetCSRF(t, session, url) - req = NewRequestWithValues(t, "POST", url, map[string]string{ + inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token) + csrf = GetCSRF(t, session, inviteURL) + req = NewRequestWithValues(t, "POST", inviteURL, map[string]string{ "_csrf": csrf, }) resp = session.MakeRequest(t, req, http.StatusSeeOther) @@ -72,6 +72,7 @@ func TestOrgTeamEmailInvite(t *testing.T) { assert.True(t, isMember) } +// Check that users are redirected to accept the invitation correctly after login func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { if setting.MailService == nil { t.Skip() @@ -92,9 +93,8 @@ func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { session := loginUser(t, "user1") teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) - csrf := GetCSRF(t, session, teamURL) req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ - "_csrf": csrf, + "_csrf": GetCSRF(t, session, teamURL), "uid": "1", "uname": user.Email, }) @@ -136,12 +136,8 @@ func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { session.jar.SetCookies(baseURL, cr.Cookies()) // make the request - req = NewRequest(t, "GET", test.RedirectURL(resp)) - session.MakeRequest(t, req, http.StatusOK) - - csrf = GetCSRF(t, session, test.RedirectURL(resp)) req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ - "_csrf": csrf, + "_csrf": GetCSRF(t, session, test.RedirectURL(resp)), }) resp = session.MakeRequest(t, req, http.StatusSeeOther) req = NewRequest(t, "GET", test.RedirectURL(resp)) @@ -152,6 +148,7 @@ func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { assert.True(t, isMember) } +// Check that newly signed up users are redirected to accept the invitation correctly func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { if setting.MailService == nil { t.Skip() @@ -167,9 +164,8 @@ func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { session := loginUser(t, "user1") teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) - csrf := GetCSRF(t, session, teamURL) req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ - "_csrf": csrf, + "_csrf": GetCSRF(t, session, teamURL), "uid": "1", "uname": "doesnotexist@example.com", }) @@ -216,9 +212,8 @@ func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { req = NewRequest(t, "GET", test.RedirectURL(resp)) session.MakeRequest(t, req, http.StatusOK) - csrf = GetCSRF(t, session, test.RedirectURL(resp)) req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ - "_csrf": csrf, + "_csrf": GetCSRF(t, session, test.RedirectURL(resp)), }) resp = session.MakeRequest(t, req, http.StatusSeeOther) req = NewRequest(t, "GET", test.RedirectURL(resp)) @@ -232,3 +227,151 @@ func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { assert.NoError(t, err) assert.True(t, isMember) } + +// Check that users are redirected correctly after confirming their email +func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) { + if setting.MailService == nil { + t.Skip() + return + } + + setting.Service.RegisterEmailConfirm = true + + defer tests.PrepareTestEnv(t)() + + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + + // create the invite + session := loginUser(t, "user1") + + teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) + req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ + "_csrf": GetCSRF(t, session, teamURL), + "uid": "1", + "uname": "doesnotexist@example.com", + }) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + // get the invite token + invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID) + assert.NoError(t, err) + assert.Len(t, invites, 1) + + // accept the invite + inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token) + req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL))) + inviteResp := MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + req = NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ + "_csrf": doc.GetCSRF(), + "user_name": "doesnotexist", + "email": "doesnotexist@example.com", + "password": "examplePassword!1", + "retype": "examplePassword!1", + }) + for _, c := range inviteResp.Result().Cookies() { + req.AddCookie(c) + } + + resp = MakeRequest(t, req, http.StatusOK) + + user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist") + assert.NoError(t, err) + + ch := http.Header{} + ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";")) + cr := http.Request{Header: ch} + + session = emptyTestSession(t) + baseURL, err := url.Parse(setting.AppURL) + assert.NoError(t, err) + session.jar.SetCookies(baseURL, cr.Cookies()) + + activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com")) + req = NewRequestWithValues(t, "POST", activateURL, map[string]string{ + "password": "examplePassword!1", + }) + + // use the cookies set by the signup request + for _, c := range inviteResp.Result().Cookies() { + req.AddCookie(c) + } + + resp = session.MakeRequest(t, req, http.StatusSeeOther) + // should be redirected to accept the invite + assert.Equal(t, inviteURL, test.RedirectURL(resp)) + + req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ + "_csrf": GetCSRF(t, session, test.RedirectURL(resp)), + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID) + assert.NoError(t, err) + assert.True(t, isMember) +} + +// Test that a logged-in user who navigates to the sign-up link is then redirected using redirect_to +// For example: an invite may have been created before the user account was created, but they may be +// accepting the invite after having created an account separately +func TestOrgTeamEmailInviteRedirectsExistingUserWithLogin(t *testing.T) { + if setting.MailService == nil { + t.Skip() + return + } + + defer tests.PrepareTestEnv(t)() + + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + + isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID) + assert.NoError(t, err) + assert.False(t, isMember) + + // create the invite + session := loginUser(t, "user1") + + teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) + req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ + "_csrf": GetCSRF(t, session, teamURL), + "uid": "1", + "uname": user.Email, + }) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + // get the invite token + invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID) + assert.NoError(t, err) + assert.Len(t, invites, 1) + + // note: the invited user has logged in + session = loginUser(t, "user5") + + // accept the invite (note: this uses the sign_up url) + inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token) + req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL))) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, inviteURL, test.RedirectURL(resp)) + + // make the request + req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ + "_csrf": GetCSRF(t, session, test.RedirectURL(resp)), + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID) + assert.NoError(t, err) + assert.True(t, isMember) +} From 6846f4e6aec4d5bc236e630bfa2bc0483a687324 Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Wed, 30 Aug 2023 16:21:41 -0400 Subject: [PATCH 10/11] cleanup --- tests/integration/org_team_invite_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go index ae46c80ec3dca..919769a61a251 100644 --- a/tests/integration/org_team_invite_test.go +++ b/tests/integration/org_team_invite_test.go @@ -209,9 +209,6 @@ func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { session.jar.SetCookies(baseURL, cr.Cookies()) // make the redirected request - req = NewRequest(t, "GET", test.RedirectURL(resp)) - session.MakeRequest(t, req, http.StatusOK) - req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ "_csrf": GetCSRF(t, session, test.RedirectURL(resp)), }) @@ -235,6 +232,10 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) { return } + // enable email confirmation temporarily + defer func(prevVal bool) { + setting.Service.RegisterEmailConfirm = prevVal + }(setting.Service.RegisterEmailConfirm) setting.Service.RegisterEmailConfirm = true defer tests.PrepareTestEnv(t)() From 9de90418f6521bc48080136a7269bf34c57cc878 Mon Sep 17 00:00:00 2001 From: jackHay22 Date: Thu, 31 Aug 2023 11:16:25 -0400 Subject: [PATCH 11/11] construct invite url outside of template --- services/mailer/mail_team_invite.go | 12 +++++++++--- templates/mail/team_invite.tmpl | 8 +------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index fdbe314c1063a..1403923c79543 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "net/url" org_model "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" @@ -42,7 +43,13 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod return fmt.Errorf("login is prohibited for the invited user") } - userAccountExists := err == nil && user != nil + inviteRedirect := url.QueryEscape(fmt.Sprintf("/org/invite/%s", invite.Token)) + inviteURL := fmt.Sprintf("%suser/sign_up?redirect_to=%s", setting.AppURL, inviteRedirect) + + if err == nil && user != nil { + // user account exists + inviteURL = fmt.Sprintf("%suser/login?redirect_to=%s", setting.AppURL, inviteRedirect) + } subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName()) mailMeta := map[string]any{ @@ -51,8 +58,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod "Team": team, "Invite": invite, "Subject": subject, - // user to determine whether to link to sign-up or login - "UserAccountExists": userAccountExists, + "InviteURL": inviteURL, // helper "locale": locale, "Str2html": templates.Str2html, diff --git a/templates/mail/team_invite.tmpl b/templates/mail/team_invite.tmpl index 120fcb438809a..d21b7843ec785 100644 --- a/templates/mail/team_invite.tmpl +++ b/templates/mail/team_invite.tmpl @@ -6,13 +6,7 @@

{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName) | Str2html}}

- {{if .UserAccountExists}} - {{$login_url := printf "%suser/login?redirect_to=%s" AppUrl (QueryEscape (printf "/org/invite/%s" .Invite.Token))}} -

{{.locale.Tr "mail.team_invite.text_2"}}

{{$login_url}}

- {{else}} - {{$signup_url := printf "%suser/sign_up?redirect_to=%s" AppUrl (QueryEscape (printf "/org/invite/%s" .Invite.Token))}} -

{{.locale.Tr "mail.team_invite.text_2"}}

{{$signup_url}}

- {{end}} +

{{.locale.Tr "mail.team_invite.text_2"}}

{{.InviteURL}}

{{.locale.Tr "mail.link_not_working_do_paste"}}

{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}