Template
1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo synced 2024-11-25 11:16:11 +01:00

Compare commits

...

3 commits

Author SHA1 Message Date
Earl Warren 7015bdfa48 Merge pull request 'chore(ci): remove unused experimental DNS updates' (#6034) from earl-warren/forgejo:wip-dns-update into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6034
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-20 15:45:12 +00:00
Earl Warren a69943085a
chore(ci): remove unused experimental DNS updates 2024-11-20 15:13:53 +00:00
JakobDev 45fa9e5ae9 fix: Allow Organisations to remove the Email Address (#5517)
It is possible to set a Email for a Organization. This Email is optional and only used to be displayed on the profile page. However, once you set an EMail, you can no longer remove it. This PR fixes that.

While working on the tests, I found out, that the API returns a 500 when trying to set an invalid EMail. I fixed that too. It returns a 422 now.

Fixes #4567

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5517
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: JakobDev <jakobdev@gmx.de>
Co-committed-by: JakobDev <jakobdev@gmx.de>
2024-11-20 12:31:34 +00:00
9 changed files with 228 additions and 27 deletions

View file

@ -84,20 +84,3 @@ jobs:
ref_name: '${{ github.ref_name }}' ref_name: '${{ github.ref_name }}'
image: 'codeberg.org/forgejo-experimental/forgejo' image: 'codeberg.org/forgejo-experimental/forgejo'
tag_suffix: '-rootless' tag_suffix: '-rootless'
- name: set up go for the DNS update below
if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != ''
uses: https://code.forgejo.org/actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: update the _release.experimental DNS record
if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != ''
uses: https://code.forgejo.org/actions/ovh-dns-update@v1
with:
subdomain: _release.experimental
domain: forgejo.com # there is a CNAME from .org to .com (for security reasons)
record-id: 5283602601
value: v=${{ github.ref_name }}
ovh-app-key: ${{ secrets.OVH_APP_KEY }}
ovh-app-secret: ${{ secrets.OVH_APP_SECRET }}
ovh-consumer-key: ${{ secrets.OVH_CON_KEY }}

View file

@ -139,6 +139,38 @@ func GetPrimaryEmailAddressOfUser(ctx context.Context, uid int64) (*EmailAddress
return ea, nil return ea, nil
} }
// Deletes the primary email address of the user
// This is only allowed if the user is a organization
func DeletePrimaryEmailAddressOfUser(ctx context.Context, uid int64) error {
user, err := GetUserByID(ctx, uid)
if err != nil {
return err
}
if user.Type != UserTypeOrganization {
return fmt.Errorf("%s is not a organization", user.Name)
}
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
_, err = db.GetEngine(ctx).Exec("DELETE FROM email_address WHERE uid = ? AND is_primary = true", uid)
if err != nil {
return err
}
user.Email = ""
err = UpdateUserCols(ctx, user, "email")
if err != nil {
return err
}
return committer.Commit()
}
// GetEmailAddresses returns all email addresses belongs to given user. // GetEmailAddresses returns all email addresses belongs to given user.
func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) { func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) {
emails := make([]*EmailAddress, 0, 5) emails := make([]*EmailAddress, 0, 5)

View file

@ -163,3 +163,21 @@ func TestGetActivatedEmailAddresses(t *testing.T) {
}) })
} }
} }
func TestDeletePrimaryEmailAddressOfUser(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
user, err := user_model.GetUserByName(db.DefaultContext, "org3")
require.NoError(t, err)
assert.Equal(t, "org3@example.com", user.Email)
require.NoError(t, user_model.DeletePrimaryEmailAddressOfUser(db.DefaultContext, user.ID))
user, err = user_model.GetUserByName(db.DefaultContext, "org3")
require.NoError(t, err)
assert.Empty(t, user.Email)
email, err := user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
assert.True(t, user_model.IsErrEmailAddressNotExist(err))
assert.Nil(t, email)
}

View file

@ -48,7 +48,7 @@ type CreateOrgOption struct {
// EditOrgOption options for editing an organization // EditOrgOption options for editing an organization
type EditOrgOption struct { type EditOrgOption struct {
FullName string `json:"full_name" binding:"MaxSize(100)"` FullName string `json:"full_name" binding:"MaxSize(100)"`
Email string `json:"email" binding:"MaxSize(255)"` Email *string `json:"email" binding:"MaxSize(255)"`
Description string `json:"description" binding:"MaxSize(255)"` Description string `json:"description" binding:"MaxSize(255)"`
Website string `json:"website" binding:"ValidUrl;MaxSize(255)"` Website string `json:"website" binding:"ValidUrl;MaxSize(255)"`
Location string `json:"location" binding:"MaxSize(50)"` Location string `json:"location" binding:"MaxSize(50)"`

View file

@ -15,6 +15,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@ -340,14 +341,29 @@ func Edit(ctx *context.APIContext) {
// "$ref": "#/responses/Organization" // "$ref": "#/responses/Organization"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
form := web.GetForm(ctx).(*api.EditOrgOption) form := web.GetForm(ctx).(*api.EditOrgOption)
if form.Email != "" { if form.Email != nil {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil { if *form.Email == "" {
ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err) err := user_model.DeletePrimaryEmailAddressOfUser(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "DeletePrimaryEmailAddressOfUser", err)
return return
} }
ctx.Org.Organization.Email = ""
} else {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), *form.Email); err != nil {
if validation.IsErrEmailInvalid(err) || validation.IsErrEmailCharIsNotSupported(err) {
ctx.Error(http.StatusUnprocessableEntity, "ReplacePrimaryEmailAddress", err)
} else {
ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err)
}
return
}
}
} }
opts := &user_service.UpdateOptions{ opts := &user_service.UpdateOptions{

View file

@ -93,7 +93,13 @@ func SettingsPost(ctx *context.Context) {
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name) ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
} }
if form.Email != "" { if form.Email == "" {
err := user_model.DeletePrimaryEmailAddressOfUser(ctx, org.ID)
if err != nil {
ctx.ServerError("DeletePrimaryEmailAddressOfUser", err)
return
}
} else {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
ctx.Data["Err_Email"] = true ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form) ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)

View file

@ -2263,6 +2263,9 @@
}, },
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/error"
} }
} }
} }

View file

@ -218,3 +218,57 @@ func TestAPIOrgSearchEmptyTeam(t *testing.T) {
assert.EqualValues(t, "Empty", data.Data[0].Name) assert.EqualValues(t, "Empty", data.Data[0].Name)
} }
} }
func TestAPIOrgChangeEmail(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
t.Run("Invalid", func(t *testing.T) {
newMail := "invalid"
settings := api.EditOrgOption{Email: &newMail}
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &settings).AddTokenAuth(token), http.StatusUnprocessableEntity)
var org *api.Organization
DecodeJSON(t, resp, &org)
assert.Empty(t, org.Email)
})
t.Run("Valid", func(t *testing.T) {
newMail := "example@example.com"
settings := api.EditOrgOption{Email: &newMail}
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &settings).AddTokenAuth(token), http.StatusOK)
var org *api.Organization
DecodeJSON(t, resp, &org)
assert.Equal(t, "example@example.com", org.Email)
})
t.Run("NoChange", func(t *testing.T) {
settings := api.EditOrgOption{}
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &settings).AddTokenAuth(token), http.StatusOK)
var org *api.Organization
DecodeJSON(t, resp, &org)
assert.Equal(t, "example@example.com", org.Email)
})
t.Run("Empty", func(t *testing.T) {
newMail := ""
settings := api.EditOrgOption{Email: &newMail}
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &settings).AddTokenAuth(token), http.StatusOK)
var org *api.Organization
DecodeJSON(t, resp, &org)
assert.Empty(t, org.Email)
})
}

View file

@ -0,0 +1,89 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func getOrgSettingsFormData(t *testing.T, session *TestSession, orgName string) map[string]string {
return map[string]string{
"_csrf": GetCSRF(t, session, fmt.Sprintf("/org/%s/settings", orgName)),
"name": orgName,
"full_name": "",
"email": "",
"description": "",
"website": "",
"location": "",
"visibility": "0",
"repo_admin_change_team_access": "on",
"max_repo_creation": "-1",
}
}
func getOrgSettings(t *testing.T, token, orgName string) *api.Organization {
t.Helper()
req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var org *api.Organization
DecodeJSON(t, resp, &org)
return org
}
func TestOrgSettingsChangeEmail(t *testing.T) {
defer tests.PrepareTestEnv(t)()
const orgName = "org3"
settingsURL := fmt.Sprintf("/org/%s/settings", orgName)
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
t.Run("Invalid", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
settings := getOrgSettingsFormData(t, session, orgName)
settings["email"] = "invalid"
session.MakeRequest(t, NewRequestWithValues(t, "POST", settingsURL, settings), http.StatusOK)
org := getOrgSettings(t, token, orgName)
assert.Equal(t, "org3@example.com", org.Email)
})
t.Run("Valid", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
settings := getOrgSettingsFormData(t, session, orgName)
settings["email"] = "example@example.com"
session.MakeRequest(t, NewRequestWithValues(t, "POST", settingsURL, settings), http.StatusSeeOther)
org := getOrgSettings(t, token, orgName)
assert.Equal(t, "example@example.com", org.Email)
})
t.Run("Empty", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
settings := getOrgSettingsFormData(t, session, orgName)
settings["email"] = ""
session.MakeRequest(t, NewRequestWithValues(t, "POST", settingsURL, settings), http.StatusSeeOther)
org := getOrgSettings(t, token, orgName)
assert.Empty(t, org.Email)
})
}