diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go index a5a16f35f..01a5796ae 100644 --- a/internal/api/client/admin/admin.go +++ b/internal/api/client/admin/admin.go @@ -102,12 +102,14 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H attachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler) attachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler) attachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler) + attachHandler(http.MethodPut, DomainBlocksPathWithID, m.DomainBlockUpdatePUTHandler) attachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler) // domain allow stuff attachHandler(http.MethodPost, DomainAllowsPath, m.DomainAllowsPOSTHandler) attachHandler(http.MethodGet, DomainAllowsPath, m.DomainAllowsGETHandler) attachHandler(http.MethodGet, DomainAllowsPathWithID, m.DomainAllowGETHandler) + attachHandler(http.MethodPut, DomainAllowsPathWithID, m.DomainAllowUpdatePUTHandler) attachHandler(http.MethodDelete, DomainAllowsPathWithID, m.DomainAllowDELETEHandler) // domain permission draft stuff diff --git a/internal/api/client/admin/domainallowupdate.go b/internal/api/client/admin/domainallowupdate.go new file mode 100644 index 000000000..02edfdfef --- /dev/null +++ b/internal/api/client/admin/domainallowupdate.go @@ -0,0 +1,91 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// DomainAllowUpdatePUTHandler swagger:operation PUT /api/v1/admin/domain_allows/{id} domainAllowUpdate +// +// Update a single domain allow. +// +// --- +// tags: +// - admin +// +// consumes: +// - multipart/form-data +// +// produces: +// - application/json +// +// parameters: +// - +// name: id +// type: string +// description: The id of the domain allow. +// in: path +// required: true +// - +// name: obfuscate +// in: formData +// description: >- +// Obfuscate the name of the domain when serving it publicly. +// Eg., `example.org` becomes something like `ex***e.org`. +// type: boolean +// - +// name: public_comment +// in: formData +// description: >- +// Public comment about this domain allow. +// This will be displayed alongside the domain allow if you choose to share allows. +// type: string +// - +// name: private_comment +// in: formData +// description: >- +// Private comment about this domain allow. Will only be shown to other admins, so this +// is a useful way of internally keeping track of why a certain domain ended up allowed. +// type: string +// +// security: +// - OAuth2 Bearer: +// - admin:write:domain_allows +// +// responses: +// '200': +// description: The updated domain allow. +// schema: +// "$ref": "#/definitions/domainPermission" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) DomainAllowUpdatePUTHandler(c *gin.Context) { + m.updateDomainPermission(c, gtsmodel.DomainPermissionAllow) +} diff --git a/internal/api/client/admin/domainblockupdate.go b/internal/api/client/admin/domainblockupdate.go new file mode 100644 index 000000000..0fbe72aa8 --- /dev/null +++ b/internal/api/client/admin/domainblockupdate.go @@ -0,0 +1,91 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package admin + +import ( + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// DomainBlockUpdatePUTHandler swagger:operation PUT /api/v1/admin/domain_blocks/{id} domainBlockUpdate +// +// Update a single domain block. +// +// --- +// tags: +// - admin +// +// consumes: +// - multipart/form-data +// +// produces: +// - application/json +// +// parameters: +// - +// name: id +// type: string +// description: The id of the domain block. +// in: path +// required: true +// - +// name: obfuscate +// in: formData +// description: >- +// Obfuscate the name of the domain when serving it publicly. +// Eg., `example.org` becomes something like `ex***e.org`. +// type: boolean +// - +// name: public_comment +// in: formData +// description: >- +// Public comment about this domain block. +// This will be displayed alongside the domain block if you choose to share blocks. +// type: string +// - +// name: private_comment +// in: formData +// description: >- +// Private comment about this domain block. Will only be shown to other admins, so this +// is a useful way of internally keeping track of why a certain domain ended up blocked. +// type: string +// +// security: +// - OAuth2 Bearer: +// - admin:write:domain_blocks +// +// responses: +// '200': +// description: The updated domain block. +// schema: +// "$ref": "#/definitions/domainPermission" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) DomainBlockUpdatePUTHandler(c *gin.Context) { + m.updateDomainPermission(c, gtsmodel.DomainPermissionBlock) +} diff --git a/internal/api/client/admin/domainpermission.go b/internal/api/client/admin/domainpermission.go index c64c90eb2..91b95334b 100644 --- a/internal/api/client/admin/domainpermission.go +++ b/internal/api/client/admin/domainpermission.go @@ -29,6 +29,7 @@ import ( apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" ) type singleDomainPermCreate func( @@ -112,7 +113,7 @@ func (m *Module) createDomainPermissions( if importing && form.Domains.Size == 0 { err = errors.New("import was specified but list of domains is empty") } else if !importing && form.Domain == "" { - err = errors.New("empty domain provided") + err = errors.New("no domain provided") } if err != nil { @@ -122,14 +123,14 @@ func (m *Module) createDomainPermissions( if !importing { // Single domain permission creation. - domainBlock, _, errWithCode := single( + perm, _, errWithCode := single( c.Request.Context(), permType, authed.Account, form.Domain, - form.Obfuscate, - form.PublicComment, - form.PrivateComment, + util.PtrOrZero(form.Obfuscate), + util.PtrOrZero(form.PublicComment), + util.PtrOrZero(form.PrivateComment), "", // No sub ID for single perm creation. ) @@ -138,7 +139,7 @@ func (m *Module) createDomainPermissions( return } - apiutil.JSON(c, http.StatusOK, domainBlock) + apiutil.JSON(c, http.StatusOK, perm) return } @@ -177,6 +178,82 @@ func (m *Module) createDomainPermissions( apiutil.JSON(c, http.StatusOK, domainPerms) } +func (m *Module) updateDomainPermission( + c *gin.Context, + permType gtsmodel.DomainPermissionType, +) { + // Scope differs based on permType. + var requireScope apiutil.Scope + if permType == gtsmodel.DomainPermissionBlock { + requireScope = apiutil.ScopeAdminWriteDomainBlocks + } else { + requireScope = apiutil.ScopeAdminWriteDomainAllows + } + + authed, errWithCode := apiutil.TokenAuth(c, + true, true, true, true, + requireScope, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + if !*authed.User.Admin { + err := fmt.Errorf("user %s not an admin", authed.User.ID) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1) + return + } + + if authed.Account.IsMoving() { + apiutil.ForbiddenAfterMove(c) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + permID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + // Parse + validate form. + form := new(apimodel.DomainPermissionRequest) + if err := c.ShouldBind(form); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + return + } + + if form.Obfuscate == nil && + form.PrivateComment == nil && + form.PublicComment == nil { + const errText = "empty form submitted" + errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + perm, errWithCode := m.processor.Admin().DomainPermissionUpdate( + c.Request.Context(), + permType, + permID, + form.Obfuscate, + form.PublicComment, + form.PrivateComment, + nil, // Can't update perm sub ID this way yet. + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.JSON(c, http.StatusOK, perm) +} + // deleteDomainPermission deletes a single domain permission (block or allow). func (m *Module) deleteDomainPermission( c *gin.Context, diff --git a/internal/api/client/admin/domainpermissiondraftcreate.go b/internal/api/client/admin/domainpermissiondraftcreate.go index b8d3085e9..e7fcd2c40 100644 --- a/internal/api/client/admin/domainpermissiondraftcreate.go +++ b/internal/api/client/admin/domainpermissiondraftcreate.go @@ -26,6 +26,7 @@ import ( apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // DomainPermissionDraftsPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts domainPermissionDraftCreate @@ -148,9 +149,9 @@ func (m *Module) DomainPermissionDraftsPOSTHandler(c *gin.Context) { authed.Account, form.Domain, permType, - form.Obfuscate, - form.PublicComment, - form.PrivateComment, + util.PtrOrZero(form.Obfuscate), + util.PtrOrZero(form.PublicComment), + util.PtrOrZero(form.PrivateComment), ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) diff --git a/internal/api/model/domain.go b/internal/api/model/domain.go index 94a190f63..9c868cfc0 100644 --- a/internal/api/model/domain.go +++ b/internal/api/model/domain.go @@ -80,14 +80,14 @@ type DomainPermissionRequest struct { // Obfuscate the domain name when displaying this permission entry publicly. // Ie., instead of 'example.org' show something like 'e**mpl*.or*'. // example: false - Obfuscate bool `form:"obfuscate" json:"obfuscate"` + Obfuscate *bool `form:"obfuscate" json:"obfuscate"` // Private comment for other admins on why this permission entry was created. // example: don't like 'em!!!! - PrivateComment string `form:"private_comment" json:"private_comment"` + PrivateComment *string `form:"private_comment" json:"private_comment"` // Public comment on why this permission entry was created. // Will be visible to requesters at /api/v1/instance/peers if this endpoint is exposed. // example: foss dorks 😫 - PublicComment string `form:"public_comment" json:"public_comment"` + PublicComment *string `form:"public_comment" json:"public_comment"` // Permission type to create (only applies to domain permission drafts, not explicit blocks and allows). PermissionType string `form:"permission_type" json:"permission_type"` } diff --git a/internal/db/bundb/domain.go b/internal/db/bundb/domain.go index 925387bd9..23b9abc74 100644 --- a/internal/db/bundb/domain.go +++ b/internal/db/bundb/domain.go @@ -36,7 +36,7 @@ type domainDB struct { state *state.State } -func (d *domainDB) CreateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) (err error) { +func (d *domainDB) PutDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) (err error) { // Normalize the domain as punycode, note the extra // validation step for domain name write operations. allow.Domain, err = util.PunifySafely(allow.Domain) @@ -162,7 +162,7 @@ func (d *domainDB) DeleteDomainAllow(ctx context.Context, domain string) error { return nil } -func (d *domainDB) CreateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error { +func (d *domainDB) PutDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error { var err error // Normalize the domain as punycode, note the extra diff --git a/internal/db/bundb/domain_test.go b/internal/db/bundb/domain_test.go index 8164259e8..a56f469c4 100644 --- a/internal/db/bundb/domain_test.go +++ b/internal/db/bundb/domain_test.go @@ -46,7 +46,7 @@ func (suite *DomainTestSuite) TestIsDomainBlocked() { suite.NoError(err) suite.False(blocked) - err = suite.db.CreateDomainBlock(ctx, domainBlock) + err = suite.db.PutDomainBlock(ctx, domainBlock) suite.NoError(err) // domain block now exists @@ -75,7 +75,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedWithAllow() { suite.False(blocked) // Block this domain. - if err := suite.db.CreateDomainBlock(ctx, domainBlock); err != nil { + if err := suite.db.PutDomainBlock(ctx, domainBlock); err != nil { suite.FailNow(err.Error()) } @@ -96,7 +96,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedWithAllow() { CreatedByAccount: suite.testAccounts["admin_account"], } - if err := suite.db.CreateDomainAllow(ctx, domainAllow); err != nil { + if err := suite.db.PutDomainAllow(ctx, domainAllow); err != nil { suite.FailNow(err.Error()) } @@ -124,7 +124,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedWildcard() { suite.NoError(err) suite.False(blocked) - err = suite.db.CreateDomainBlock(ctx, domainBlock) + err = suite.db.PutDomainBlock(ctx, domainBlock) suite.NoError(err) // Start with the base block domain @@ -164,7 +164,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedNonASCII() { suite.NoError(err) suite.False(blocked) - err = suite.db.CreateDomainBlock(ctx, domainBlock) + err = suite.db.PutDomainBlock(ctx, domainBlock) suite.NoError(err) // domain block now exists @@ -200,7 +200,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedNonASCII2() { suite.NoError(err) suite.False(blocked) - err = suite.db.CreateDomainBlock(ctx, domainBlock) + err = suite.db.PutDomainBlock(ctx, domainBlock) suite.NoError(err) // domain block now exists @@ -232,7 +232,7 @@ func (suite *DomainTestSuite) TestIsOtherDomainBlockedWildcardAndExplicit() { } for _, block := range blocks { - if err := suite.db.CreateDomainBlock(ctx, block); err != nil { + if err := suite.db.PutDomainBlock(ctx, block); err != nil { suite.FailNow(err.Error()) } } diff --git a/internal/db/bundb/domainpermissionsubscription_test.go b/internal/db/bundb/domainpermissionsubscription_test.go index 732befbff..7a5cf8685 100644 --- a/internal/db/bundb/domainpermissionsubscription_test.go +++ b/internal/db/bundb/domainpermissionsubscription_test.go @@ -80,7 +80,7 @@ func (suite *DomainPermissionSubscriptionTestSuite) TestCount() { // Whack the perms in the db. for _, perm := range perms { - if err := suite.state.DB.CreateDomainBlock(ctx, perm); err != nil { + if err := suite.state.DB.PutDomainBlock(ctx, perm); err != nil { suite.FailNow(err.Error()) } } diff --git a/internal/db/domain.go b/internal/db/domain.go index 643538e7e..95a2f0755 100644 --- a/internal/db/domain.go +++ b/internal/db/domain.go @@ -31,8 +31,8 @@ type Domain interface { Block/allow storage + retrieval functions. */ - // CreateDomainAllow puts the given instance-level domain allow into the database. - CreateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) error + // PutDomainAllow puts the given instance-level domain allow into the database. + PutDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) error // GetDomainAllow returns one instance-level domain allow with the given domain, if it exists. GetDomainAllow(ctx context.Context, domain string) (*gtsmodel.DomainAllow, error) @@ -49,8 +49,8 @@ type Domain interface { // DeleteDomainAllow deletes an instance-level domain allow with the given domain, if it exists. DeleteDomainAllow(ctx context.Context, domain string) error - // CreateDomainBlock puts the given instance-level domain block into the database. - CreateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error + // PutDomainBlock puts the given instance-level domain block into the database. + PutDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error // GetDomainBlock returns one instance-level domain block with the given domain, if it exists. GetDomainBlock(ctx context.Context, domain string) (*gtsmodel.DomainBlock, error) diff --git a/internal/gtsmodel/domainallow.go b/internal/gtsmodel/domainallow.go index 3a7ca8774..f6aedbbba 100644 --- a/internal/gtsmodel/domainallow.go +++ b/internal/gtsmodel/domainallow.go @@ -26,7 +26,7 @@ type DomainAllow struct { UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated Domain string `bun:",nullzero,notnull"` // domain to allow. Eg. 'whatever.com' CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this allow - CreatedByAccount *Account `bun:"rel:belongs-to"` // Account corresponding to createdByAccountID + CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID PrivateComment string `bun:""` // Private comment on this allow, viewable to admins PublicComment string `bun:""` // Public comment on this allow, viewable (optionally) by everyone Obfuscate *bool `bun:",nullzero,notnull,default:false"` // whether the domain name should appear obfuscated when displaying it publicly diff --git a/internal/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go index 4a0e1c5b7..fb0921c25 100644 --- a/internal/gtsmodel/domainblock.go +++ b/internal/gtsmodel/domainblock.go @@ -26,7 +26,7 @@ type DomainBlock struct { UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated Domain string `bun:",nullzero,notnull"` // domain to block. Eg. 'whatever.com' CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this block - CreatedByAccount *Account `bun:"rel:belongs-to"` // Account corresponding to createdByAccountID + CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID PrivateComment string `bun:""` // Private comment on this block, viewable to admins PublicComment string `bun:""` // Public comment on this block, viewable (optionally) by everyone Obfuscate *bool `bun:",nullzero,notnull,default:false"` // whether the domain name should appear obfuscated when displaying it publicly diff --git a/internal/processing/admin/domainallow.go b/internal/processing/admin/domainallow.go index 02101ccff..134351ad5 100644 --- a/internal/processing/admin/domainallow.go +++ b/internal/processing/admin/domainallow.go @@ -60,7 +60,7 @@ func (p *Processor) createDomainAllow( } // Insert the new allow into the database. - if err := p.state.DB.CreateDomainAllow(ctx, domainAllow); err != nil { + if err := p.state.DB.PutDomainAllow(ctx, domainAllow); err != nil { err = gtserror.Newf("db error putting domain allow %s: %w", domain, err) return nil, "", gtserror.NewErrorInternalError(err) } @@ -92,6 +92,54 @@ func (p *Processor) createDomainAllow( return apiDomainAllow, action.ID, nil } +func (p *Processor) updateDomainAllow( + ctx context.Context, + domainAllowID string, + obfuscate *bool, + publicComment *string, + privateComment *string, + subscriptionID *string, +) (*apimodel.DomainPermission, gtserror.WithCode) { + domainAllow, err := p.state.DB.GetDomainAllowByID(ctx, domainAllowID) + if err != nil { + if !errors.Is(err, db.ErrNoEntries) { + // Real error. + err = gtserror.Newf("db error getting domain allow: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + // There are just no entries for this ID. + err = fmt.Errorf("no domain allow entry exists with ID %s", domainAllowID) + return nil, gtserror.NewErrorNotFound(err, err.Error()) + } + + var columns []string + if obfuscate != nil { + domainAllow.Obfuscate = obfuscate + columns = append(columns, "obfuscate") + } + if publicComment != nil { + domainAllow.PublicComment = *publicComment + columns = append(columns, "public_comment") + } + if privateComment != nil { + domainAllow.PrivateComment = *privateComment + columns = append(columns, "private_comment") + } + if subscriptionID != nil { + domainAllow.SubscriptionID = *subscriptionID + columns = append(columns, "subscription_id") + } + + // Update the domain allow. + if err := p.state.DB.UpdateDomainAllow(ctx, domainAllow, columns...); err != nil { + err = gtserror.Newf("db error updating domain allow: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return p.apiDomainPerm(ctx, domainAllow, false) +} + func (p *Processor) deleteDomainAllow( ctx context.Context, adminAcct *gtsmodel.Account, diff --git a/internal/processing/admin/domainblock.go b/internal/processing/admin/domainblock.go index 249df744c..3dd5a256f 100644 --- a/internal/processing/admin/domainblock.go +++ b/internal/processing/admin/domainblock.go @@ -60,7 +60,7 @@ func (p *Processor) createDomainBlock( } // Insert the new block into the database. - if err := p.state.DB.CreateDomainBlock(ctx, domainBlock); err != nil { + if err := p.state.DB.PutDomainBlock(ctx, domainBlock); err != nil { err = gtserror.Newf("db error putting domain block %s: %w", domain, err) return nil, "", gtserror.NewErrorInternalError(err) } @@ -93,6 +93,54 @@ func (p *Processor) createDomainBlock( return apiDomainBlock, action.ID, nil } +func (p *Processor) updateDomainBlock( + ctx context.Context, + domainBlockID string, + obfuscate *bool, + publicComment *string, + privateComment *string, + subscriptionID *string, +) (*apimodel.DomainPermission, gtserror.WithCode) { + domainBlock, err := p.state.DB.GetDomainBlockByID(ctx, domainBlockID) + if err != nil { + if !errors.Is(err, db.ErrNoEntries) { + // Real error. + err = gtserror.Newf("db error getting domain block: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + // There are just no entries for this ID. + err = fmt.Errorf("no domain block entry exists with ID %s", domainBlockID) + return nil, gtserror.NewErrorNotFound(err, err.Error()) + } + + var columns []string + if obfuscate != nil { + domainBlock.Obfuscate = obfuscate + columns = append(columns, "obfuscate") + } + if publicComment != nil { + domainBlock.PublicComment = *publicComment + columns = append(columns, "public_comment") + } + if privateComment != nil { + domainBlock.PrivateComment = *privateComment + columns = append(columns, "private_comment") + } + if subscriptionID != nil { + domainBlock.SubscriptionID = *subscriptionID + columns = append(columns, "subscription_id") + } + + // Update the domain block. + if err := p.state.DB.UpdateDomainBlock(ctx, domainBlock, columns...); err != nil { + err = gtserror.Newf("db error updating domain block: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return p.apiDomainPerm(ctx, domainBlock, false) +} + func (p *Processor) deleteDomainBlock( ctx context.Context, adminAcct *gtsmodel.Account, diff --git a/internal/processing/admin/domainpermission.go b/internal/processing/admin/domainpermission.go index 55800f458..c2906819b 100644 --- a/internal/processing/admin/domainpermission.go +++ b/internal/processing/admin/domainpermission.go @@ -84,6 +84,50 @@ func (p *Processor) DomainPermissionCreate( } } +// DomainPermissionUpdate updates a domain permission +// of the given permissionType, with the given ID. +func (p *Processor) DomainPermissionUpdate( + ctx context.Context, + permissionType gtsmodel.DomainPermissionType, + permID string, + obfuscate *bool, + publicComment *string, + privateComment *string, + subscriptionID *string, +) (*apimodel.DomainPermission, gtserror.WithCode) { + switch permissionType { + + // Explicitly block a domain. + case gtsmodel.DomainPermissionBlock: + return p.updateDomainBlock( + ctx, + permID, + obfuscate, + publicComment, + privateComment, + subscriptionID, + ) + + // Explicitly allow a domain. + case gtsmodel.DomainPermissionAllow: + return p.updateDomainAllow( + ctx, + permID, + obfuscate, + publicComment, + privateComment, + subscriptionID, + ) + + // 🎵 Why don't we all strap bombs to our chests, + // and ride our bikes to the next G7 picnic? + // Seems easier with every clock-tick. 🎵 + default: + err := gtserror.Newf("unrecognized permission type %d", permissionType) + return nil, gtserror.NewErrorInternalError(err) + } +} + // DomainPermissionDelete removes one domain block with the given ID, // and processes side effects of removing the block asynchronously. // diff --git a/internal/subscriptions/domainperms.go b/internal/subscriptions/domainperms.go index c9f569f94..8ac758a67 100644 --- a/internal/subscriptions/domainperms.go +++ b/internal/subscriptions/domainperms.go @@ -438,7 +438,7 @@ func (s *Subscriptions) processDomainPermission( Obfuscate: wantedPerm.GetObfuscate(), SubscriptionID: permSub.ID, } - insertF = func() error { return s.state.DB.CreateDomainBlock(ctx, domainBlock) } + insertF = func() error { return s.state.DB.PutDomainBlock(ctx, domainBlock) } action = >smodel.AdminAction{ ID: id.NewULID(), @@ -461,7 +461,7 @@ func (s *Subscriptions) processDomainPermission( Obfuscate: wantedPerm.GetObfuscate(), SubscriptionID: permSub.ID, } - insertF = func() error { return s.state.DB.CreateDomainAllow(ctx, domainAllow) } + insertF = func() error { return s.state.DB.PutDomainAllow(ctx, domainAllow) } action = >smodel.AdminAction{ ID: id.NewULID(), diff --git a/internal/subscriptions/subscriptions_test.go b/internal/subscriptions/subscriptions_test.go index 133db4b7c..4441d8c15 100644 --- a/internal/subscriptions/subscriptions_test.go +++ b/internal/subscriptions/subscriptions_test.go @@ -775,7 +775,7 @@ func (suite *SubscriptionsTestSuite) TestAdoption() { existingBlock2, existingBlock3, } { - if err := testStructs.State.DB.CreateDomainBlock( + if err := testStructs.State.DB.PutDomainBlock( ctx, block, ); err != nil { suite.FailNow(err.Error()) @@ -876,7 +876,7 @@ func (suite *SubscriptionsTestSuite) TestDomainAllowsAndBlocks() { } // Store existing allow. - if err := testStructs.State.DB.CreateDomainAllow(ctx, existingAllow); err != nil { + if err := testStructs.State.DB.PutDomainAllow(ctx, existingAllow); err != nil { suite.FailNow(err.Error()) } diff --git a/web/source/settings/lib/query/admin/domain-permissions/update.ts b/web/source/settings/lib/query/admin/domain-permissions/update.ts index a6b4b2039..cb657d09c 100644 --- a/web/source/settings/lib/query/admin/domain-permissions/update.ts +++ b/web/source/settings/lib/query/admin/domain-permissions/update.ts @@ -22,6 +22,7 @@ import { gtsApi } from "../../gts-api"; import { replaceCacheOnMutation, removeFromCacheOnMutation, + updateCacheOnMutation, } from "../../query-modifiers"; import { listToKeyedObject } from "../../transforms"; import type { @@ -55,6 +56,36 @@ const extended = gtsApi.injectEndpoints({ ...replaceCacheOnMutation("domainAllows") }), + updateDomainBlock: build.mutation({ + query: (formData) => ({ + method: "PUT", + url: `/api/v1/admin/domain_blocks/${formData.id}`, + asForm: true, + body: formData, + discardEmpty: false + }), + ...updateCacheOnMutation("domainBlocks", { + key: (_draft, newData) => { + return newData.domain; + } + }) + }), + + updateDomainAllow: build.mutation({ + query: (formData) => ({ + method: "PUT", + url: `/api/v1/admin/domain_allows/${formData.id}`, + asForm: true, + body: formData, + discardEmpty: false + }), + ...updateCacheOnMutation("domainAllows", { + key: (_draft, newData) => { + return newData.domain; + } + }) + }), + removeDomainBlock: build.mutation({ query: (id) => ({ method: "DELETE", @@ -91,6 +122,16 @@ const useAddDomainBlockMutation = extended.useAddDomainBlockMutation; */ const useAddDomainAllowMutation = extended.useAddDomainAllowMutation; +/** + * Update a single domain permission (block) by PUTing to `/api/v1/admin/domain_blocks/{id}`. + */ +const useUpdateDomainBlockMutation = extended.useUpdateDomainBlockMutation; + +/** + * Update a single domain permission (allow) by PUTing to `/api/v1/admin/domain_allows/{id}`. + */ +const useUpdateDomainAllowMutation = extended.useUpdateDomainAllowMutation; + /** * Remove a single domain permission (block) by DELETEing to `/api/v1/admin/domain_blocks/{id}`. */ @@ -104,6 +145,8 @@ const useRemoveDomainAllowMutation = extended.useRemoveDomainAllowMutation; export { useAddDomainBlockMutation, useAddDomainAllowMutation, + useUpdateDomainBlockMutation, + useUpdateDomainAllowMutation, useRemoveDomainBlockMutation, useRemoveDomainAllowMutation }; diff --git a/web/source/settings/style.css b/web/source/settings/style.css index fc146cdd7..6169cab78 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -1406,6 +1406,7 @@ button.tab-button { } } +.domain-permission-details, .domain-permission-draft-details, .domain-permission-exclude-details, .domain-permission-subscription-details { @@ -1414,6 +1415,7 @@ button.tab-button { } } +.domain-permission-details, .domain-permission-drafts-view, .domain-permission-draft-details, .domain-permission-subscriptions-view, diff --git a/web/source/settings/views/moderation/domain-permissions/detail.tsx b/web/source/settings/views/moderation/domain-permissions/detail.tsx index 0105d9615..1486cdddf 100644 --- a/web/source/settings/views/moderation/domain-permissions/detail.tsx +++ b/web/source/settings/views/moderation/domain-permissions/detail.tsx @@ -32,8 +32,18 @@ import Loading from "../../../components/loading"; import BackButton from "../../../components/back-button"; import MutationButton from "../../../components/form/mutation-button"; -import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../../lib/query/admin/domain-permissions/get"; -import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../../lib/query/admin/domain-permissions/update"; +import { + useDomainAllowsQuery, + useDomainBlocksQuery, +} from "../../../lib/query/admin/domain-permissions/get"; +import { + useAddDomainAllowMutation, + useAddDomainBlockMutation, + useRemoveDomainAllowMutation, + useRemoveDomainBlockMutation, + useUpdateDomainAllowMutation, + useUpdateDomainBlockMutation, +} from "../../../lib/query/admin/domain-permissions/update"; import { DomainPerm } from "../../../lib/types/domain-permission"; import { NoArg } from "../../../lib/types/query"; import { Error } from "../../../components/error"; @@ -41,8 +51,9 @@ import { useBaseUrl } from "../../../lib/navigation/util"; import { PermType } from "../../../lib/types/perm"; import { useCapitalize } from "../../../lib/util"; import { formDomainValidator } from "../../../lib/util/formvalidators"; +import UsernameLozenge from "../../../components/username-lozenge"; -export default function DomainPermDetail() { +export default function DomainPermView() { const baseUrl = useBaseUrl(); const search = useSearch(); @@ -101,33 +112,16 @@ export default function DomainPermDetail() { ? blocks[domain] : allows[domain]; - // Render different into content depending on - // if we have a perm already for this domain. - let infoContent: React.JSX.Element; - if (existingPerm === undefined) { - infoContent = ( - - No stored {permType} yet, you can add one below: - - ); - } else { - infoContent = ( -
- - Editing existing domain {permTypeRaw} isn't implemented yet, check here for progress -
- ); - } + const title = Domain {permType} for {domain}; return ( -
-

- - {" "} - Domain {permType} for {domain} -

- {infoContent} - +

{title}

+ { existingPerm + ? + : No stored {permType} yet, you can add one below: + } + { + if (perm.created_at) { + return new Date(perm.created_at).toDateString(); + } + return "unknown"; + }, [perm.created_at]); + + return ( +
+
+
Created
+
+
+
+
Created By
+
+ +
+
+
+
Domain
+
{perm.domain}
+
+
+
Permission type
+
+ + {permType} +
+
+
+
Subscription ID
+
{perm.subscription_id ?? "[none]"}
+
+
+ ); +} + +interface CreateOrUpdateDomainPermProps { defaultDomain: string; perm?: DomainPerm; permType: PermType; } -function DomainPermForm({ defaultDomain, perm, permType }: DomainPermFormProps) { +function CreateOrUpdateDomainPerm({ + defaultDomain, + perm, + permType +}: CreateOrUpdateDomainPermProps) { const isExistingPerm = perm !== undefined; - const disabledForm = isExistingPerm - ? { - disabled: true, - title: "Domain permissions currently cannot be edited." - } - : { - disabled: false, - title: "", - }; const form = { domain: useTextInput("domain", { @@ -171,70 +217,80 @@ function DomainPermForm({ defaultDomain, perm, permType }: DomainPermFormProps) // react is like "weh" (mood), but we can decide // which ones to use conditionally. const [ addBlock, addBlockResult ] = useAddDomainBlockMutation(); + const [ updateBlock, updateBlockResult ] = useUpdateDomainBlockMutation({ fixedCacheKey: perm?.id }); const [ removeBlock, removeBlockResult] = useRemoveDomainBlockMutation({ fixedCacheKey: perm?.id }); const [ addAllow, addAllowResult ] = useAddDomainAllowMutation(); + const [ updateAllow, updateAllowResult ] = useUpdateDomainAllowMutation({ fixedCacheKey: perm?.id }); const [ removeAllow, removeAllowResult ] = useRemoveDomainAllowMutation({ fixedCacheKey: perm?.id }); const [ - addTrigger, - addResult, + createOrUpdateTrigger, + createOrUpdateResult, removeTrigger, removeResult, ] = useMemo(() => { - return permType == "block" - ? [ - addBlock, - addBlockResult, - removeBlock, - removeBlockResult, - ] - : [ - addAllow, - addAllowResult, - removeAllow, - removeAllowResult, - ]; - }, [permType, - addBlock, addBlockResult, removeBlock, removeBlockResult, - addAllow, addAllowResult, removeAllow, removeAllowResult, + switch (true) { + case (permType === "block" && !isExistingPerm): + return [ addBlock, addBlockResult, removeBlock, removeBlockResult ]; + case (permType === "block"): + return [ updateBlock, updateBlockResult, removeBlock, removeBlockResult ]; + case !isExistingPerm: + return [ addAllow, addAllowResult, removeAllow, removeAllowResult ]; + default: + return [ updateAllow, updateAllowResult, removeAllow, removeAllowResult ]; + } + }, [permType, isExistingPerm, + addBlock, addBlockResult, updateBlock, updateBlockResult, removeBlock, removeBlockResult, + addAllow, addAllowResult, updateAllow, updateAllowResult, removeAllow, removeAllowResult, ]); - // Use appropriate submission params for this permType. - const [submitForm, submitFormResult] = useFormSubmit(form, [addTrigger, addResult], { changedOnly: false }); + // Use appropriate submission params for this + // permType, and whether we're creating or updating. + const [submit, submitResult] = useFormSubmit( + form, + [ + createOrUpdateTrigger, + createOrUpdateResult, + ], + { + changedOnly: isExistingPerm + }, + ); // Uppercase first letter of given permType. const permTypeUpper = useCapitalize(permType); const [location, setLocation] = useLocation(); - function verifyUrlThenSubmit(e) { // Adding a new domain permissions happens on a url like // "/settings/admin/domain-permissions/:permType/domain.com", // but if domain input changes, that doesn't match anymore // and causes issues later on so, before submitting the form, // silently change url, and THEN submit. - let correctUrl = `/${permType}s/${form.domain.value}`; - if (location != correctUrl) { - setLocation(correctUrl); + if (!isExistingPerm) { + let correctUrl = `/${permType}s/${form.domain.value}`; + if (location != correctUrl) { + setLocation(correctUrl); + } } - return submitForm(e); + return submit(e); } return (
- + { !isExistingPerm && + + }