diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go new file mode 100644 index 0000000..0ef1804 --- /dev/null +++ b/internal/api/client/instance/instancepatch.go @@ -0,0 +1,50 @@ +package instance + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) { + l := m.log.WithField("func", "InstanceUpdatePATCHHandler") + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + // only admins can update instance settings + if !authed.User.Admin { + l.Debug("user is not an admin so cannot update instance settings") + c.JSON(http.StatusUnauthorized, gin.H{"error": "not an admin"}) + return + } + + l.Debugf("parsing request form %s", c.Request.Form) + form := &model.InstanceSettingsUpdateRequest{} + if err := c.ShouldBind(&form); err != nil || form == nil { + l.Debugf("could not parse form from request: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // if everything on the form is nil, then nothing has been set and we shouldn't continue + if form.SiteTitle == nil && form.RegistrationsMode == nil && form.SiteContactUsername == nil && form.SiteContactEmail == nil && form.SiteShortDescription == nil && form.SiteDescription == nil && form.SiteTerms == nil && form.Avatar == nil && form.Header == nil { + l.Debugf("could not parse form from request") + c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"}) + return + } + + i, errWithCode := m.processor.InstancePatch(form) + if errWithCode != nil { + l.Debugf("error with instance patch request: %s", errWithCode.Error()) + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, i) +} diff --git a/internal/api/model/admin.go b/internal/api/model/admin.go index c8d4c0c..036218f 100644 --- a/internal/api/model/admin.go +++ b/internal/api/model/admin.go @@ -18,8 +18,6 @@ package model -import "mime/multipart" - // AdminAccountInfo represents the *admin* view of an account's details. See here: https://docs.joinmastodon.org/entities/admin-account/ type AdminAccountInfo struct { // The ID of the account in the database. @@ -81,24 +79,3 @@ type AdminReportInfo struct { // Statuses attached to the report, for context. Statuses []Status `json:"statuses"` } - -// AdminSiteSettings is the form to be parsed on a POST or PATCH to /admin/settings -type AdminSiteSettings struct { - FormAdminSettings FormAdminSettings `form:"form_admin_settings" json:"form_admin_settings" xml:"form_admin_settings"` -} - -// FormAdminSettings wraps a whole bunch of instance settings -type FormAdminSettings struct { - SiteTitle *string `form:"site_title" json:"site_title" xml:"site_title"` - RegistrationsMode *string `form:"registrations_mode" json:"registrations_mode" xml:"registrations_mode"` - SiteContactUsername *string `form:"site_contact_username" json:"site_contact_username" xml:"site_contact_username"` - SiteContactEmail *string `form:"site_contact_email" json:"site_contact_email" xml:"site_contact_email"` - SiteShortDescription *string `form:"site_short_description" json:"site_short_description" xml:"site_short_description"` - SiteDescription *string `form:"site_description" json:"site_description" xml:"site_description"` - SiteExtendedDescription *string `form:"site_extended_description" json:"site_extended_description" xml:"site_extended_description"` - SiteTerms *string `form:"site_terms" json:"site_terms" xml:"site_terms"` - Thumbnail *multipart.FileHeader `form:"thumbnail" json:"thumbnail" xml:"thumbnail"` - Hero *multipart.FileHeader `form:"hero" json:"hero" xml:"hero"` - Mascot *multipart.FileHeader `form:"mascot" json:"mascot" xml:"mascot"` - RequireInviteText *bool `form:"require_invite_text" json:"require_invite_text" xml:"require_invite_text"` -} diff --git a/internal/api/model/instance.go b/internal/api/model/instance.go index 3552ead..7589d8c 100644 --- a/internal/api/model/instance.go +++ b/internal/api/model/instance.go @@ -18,6 +18,8 @@ package model +import "mime/multipart" + // Instance represents the software instance of Mastodon running on this domain. See https://docs.joinmastodon.org/entities/instance/ type Instance struct { // REQUIRED @@ -70,3 +72,16 @@ type InstanceStats struct { // Domains federated with this instance. DomainCount int `json:"domain_count"` } + +// InstanceSettingsUpdateRequest is the form to be parsed on a PATCH to /api/v1/instance +type InstanceSettingsUpdateRequest struct { + SiteTitle *string `form:"site_title" json:"site_title" xml:"site_title"` + RegistrationsMode *string `form:"registrations_mode" json:"registrations_mode" xml:"registrations_mode"` + SiteContactUsername *string `form:"site_contact_username" json:"site_contact_username" xml:"site_contact_username"` + SiteContactEmail *string `form:"site_contact_email" json:"site_contact_email" xml:"site_contact_email"` + SiteShortDescription *string `form:"site_short_description" json:"site_short_description" xml:"site_short_description"` + SiteDescription *string `form:"site_description" json:"site_description" xml:"site_description"` + SiteTerms *string `form:"site_terms" json:"site_terms" xml:"site_terms"` + Avatar *multipart.FileHeader `form:"avatar" json:"avatar" xml:"avatar"` + Header *multipart.FileHeader `form:"header" json:"header" xml:"header"` +} diff --git a/internal/processing/instance.go b/internal/processing/instance.go index 9381a73..7efa225 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -40,3 +40,17 @@ func (p *processor) InstanceGet(domain string) (*apimodel.Instance, gtserror.Wit return ai, nil } + +func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.Instance, gtserror.WithCode) { + i := >smodel.Instance{} + if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: p.config.Host}}, i); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err)) + } + + ai, err := p.tc.InstanceToMasto(i) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err)) + } +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + return ai, nil +} diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 618fd64..2cfa6e4 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -95,6 +95,10 @@ type Processor interface { // InstanceGet retrieves instance information for serving at api/v1/instance InstanceGet(domain string) (*apimodel.Instance, gtserror.WithCode) + // InstancePatch updates this instance according to the given form. + // + // It should already be ascertained that the requesting account is authenticated and an admin. + InstancePatch(form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.Instance, gtserror.WithCode) // MediaCreate handles the creation of a media attachment, using the given form. MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) diff --git a/internal/util/regexes.go b/internal/util/regexes.go index 586eb30..416d66e 100644 --- a/internal/util/regexes.go +++ b/internal/util/regexes.go @@ -24,12 +24,8 @@ import ( ) const ( - minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator - minimumReasonLength = 40 - maximumReasonLength = 500 maximumEmailLength = 256 maximumUsernameLength = 64 - maximumPasswordLength = 64 maximumEmojiShortcodeLength = 30 maximumHashtagLength = 30 ) diff --git a/internal/util/validation.go b/internal/util/validation.go index d392231..446f7a7 100644 --- a/internal/util/validation.go +++ b/internal/util/validation.go @@ -27,6 +27,17 @@ import ( "golang.org/x/text/language" ) +const ( + maximumPasswordLength = 64 + minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator + minimumReasonLength = 40 + maximumReasonLength = 500 + maximumSiteTitleLength = 40 + maximumShortDescriptionLength = 500 + maximumDescriptionLength = 5000 + maximumSiteTermsLength = 5000 +) + // ValidateNewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok. func ValidateNewPassword(password string) error { if password == "" { @@ -47,12 +58,8 @@ func ValidateUsername(username string) error { return errors.New("no username provided") } - if len(username) > maximumUsernameLength { - return fmt.Errorf("username should be no more than %d chars but '%s' was %d", maximumUsernameLength, username, len(username)) - } - if !usernameValidationRegex.MatchString(username) { - return fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", username) + return fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max %d characters", username, maximumUsernameLength) } return nil @@ -65,10 +72,6 @@ func ValidateEmail(email string) error { return errors.New("no email provided") } - if len(email) > maximumEmailLength { - return fmt.Errorf("email address should be no more than %d chars but '%s' was %d", maximumEmailLength, email, len(email)) - } - _, err := mail.ParseAddress(email) return err } @@ -132,3 +135,39 @@ func ValidateEmojiShortcode(shortcode string) error { } return nil } + +// ValidateSiteTitle ensures that the given site title is within spec. +func ValidateSiteTitle(siteTitle string) error { + if len(siteTitle) > maximumSiteTitleLength { + return fmt.Errorf("site title should be no more than %d chars but given title was %d", maximumSiteTitleLength, len(siteTitle)) + } + + return nil +} + +// ValidateSiteShortDescription ensures that the given site short description is within spec. +func ValidateSiteShortDescription(d string) error { + if len(d) > maximumShortDescriptionLength { + return fmt.Errorf("short description should be no more than %d chars but given description was %d", maximumShortDescriptionLength, len(d)) + } + + return nil +} + +// ValidateSiteDescription ensures that the given site description is within spec. +func ValidateSiteDescription(d string) error { + if len(d) > maximumDescriptionLength { + return fmt.Errorf("description should be no more than %d chars but given description was %d", maximumDescriptionLength, len(d)) + } + + return nil +} + +// ValidateSiteTerms ensures that the given site terms string is within spec. +func ValidateSiteTerms(t string) error { + if len(t) > maximumSiteTermsLength { + return fmt.Errorf("terms should be no more than %d chars but given terms was %d", maximumSiteTermsLength, len(t)) + } + + return nil +}