Go · 6580 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package orgs
4
5 import (
6 "net/http"
7 "net/mail"
8 "net/url"
9 "strings"
10 "unicode/utf8"
11
12 orgdomain "github.com/tenseleyFlow/shithub/internal/orgs"
13 orgsdb "github.com/tenseleyFlow/shithub/internal/orgs/sqlc"
14 "github.com/tenseleyFlow/shithub/internal/web/middleware"
15 )
16
17 const (
18 orgProfileMaxDisplayName = 100
19 orgProfileMaxLocation = 80
20 orgProfileMaxWebsite = 200
21 )
22
23 type settingsProfileForm struct {
24 DisplayName string
25 Description string
26 Website string
27 Location string
28 BillingEmail string
29 AllowMemberRepoCreate bool
30 }
31
32 func settingsProfileFormFromOrg(org orgsdb.Org) settingsProfileForm {
33 return settingsProfileForm{
34 DisplayName: org.DisplayName,
35 Description: org.Description,
36 Website: org.Website,
37 Location: org.Location,
38 BillingEmail: org.BillingEmail,
39 AllowMemberRepoCreate: org.AllowMemberRepoCreate,
40 }
41 }
42
43 func (h *Handlers) settingsProfileSubmit(w http.ResponseWriter, r *http.Request) {
44 org, ok := h.orgFromSlug(w, r)
45 if !ok {
46 return
47 }
48 viewer := middleware.CurrentUserFromContext(r.Context())
49 if !h.requireOrgOwner(w, r, org.ID, viewer) {
50 return
51 }
52 if err := r.ParseForm(); err != nil {
53 h.d.Render.HTTPError(w, r, http.StatusBadRequest, "form parse")
54 return
55 }
56
57 form := settingsProfileForm{
58 DisplayName: strings.TrimSpace(r.PostFormValue("display_name")),
59 Description: strings.TrimRight(r.PostFormValue("description"), " \t\r\n"),
60 Website: strings.TrimSpace(r.PostFormValue("website")),
61 Location: strings.TrimSpace(r.PostFormValue("location")),
62 BillingEmail: strings.TrimSpace(r.PostFormValue("billing_email")),
63 AllowMemberRepoCreate: r.PostFormValue("allow_member_repo_create") == "on",
64 }
65 if form.DisplayName == "" {
66 form.DisplayName = org.Slug
67 }
68 if msg := validateOrgProfile(&form); msg != "" {
69 h.renderSettingsProfileWithForm(w, r, org, form, msg, "")
70 return
71 }
72
73 q := orgsdb.New()
74 tx, err := h.d.Pool.Begin(r.Context())
75 if err != nil {
76 h.d.Logger.ErrorContext(r.Context(), "org settings: begin update", "error", err)
77 h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
78 return
79 }
80 committed := false
81 defer func() {
82 if !committed {
83 _ = tx.Rollback(r.Context())
84 }
85 }()
86 if err := q.UpdateOrgProfile(r.Context(), tx, orgsdb.UpdateOrgProfileParams{
87 ID: org.ID,
88 DisplayName: form.DisplayName,
89 Description: form.Description,
90 Location: form.Location,
91 Website: form.Website,
92 BillingEmail: form.BillingEmail,
93 }); err != nil {
94 h.d.Logger.ErrorContext(r.Context(), "org settings: update profile", "error", err)
95 h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
96 return
97 }
98 if err := q.SetOrgAllowMemberRepoCreate(r.Context(), tx, orgsdb.SetOrgAllowMemberRepoCreateParams{
99 ID: org.ID,
100 AllowMemberRepoCreate: form.AllowMemberRepoCreate,
101 }); err != nil {
102 h.d.Logger.ErrorContext(r.Context(), "org settings: update member repo create", "error", err)
103 h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
104 return
105 }
106 if err := tx.Commit(r.Context()); err != nil {
107 h.d.Logger.ErrorContext(r.Context(), "org settings: commit update", "error", err)
108 h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
109 return
110 }
111 committed = true
112 updated, err := q.GetOrgByID(r.Context(), h.d.Pool, org.ID)
113 if err != nil {
114 h.d.Logger.ErrorContext(r.Context(), "org settings: reload profile", "error", err)
115 h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
116 return
117 }
118 h.renderSettingsProfile(w, r, updated, "", "Organization profile updated.")
119 }
120
121 func (h *Handlers) settingsDelete(w http.ResponseWriter, r *http.Request) {
122 org, ok := h.orgFromSlug(w, r)
123 if !ok {
124 return
125 }
126 viewer := middleware.CurrentUserFromContext(r.Context())
127 if !h.requireOrgOwner(w, r, org.ID, viewer) {
128 return
129 }
130 if err := r.ParseForm(); err != nil {
131 h.d.Render.HTTPError(w, r, http.StatusBadRequest, "form parse")
132 return
133 }
134 if !strings.EqualFold(strings.TrimSpace(r.PostFormValue("confirm_slug")), org.Slug) {
135 h.renderSettingsProfile(w, r, org, "Enter this organization's name to confirm deletion.", "")
136 return
137 }
138 if err := orgdomain.SoftDelete(r.Context(), h.deps(), org.ID, viewer.ID); err != nil {
139 h.d.Logger.ErrorContext(r.Context(), "org settings: soft delete", "error", err)
140 h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
141 return
142 }
143 http.Redirect(w, r, "/settings/organizations", http.StatusSeeOther)
144 }
145
146 func (h *Handlers) renderSettingsProfileWithForm(
147 w http.ResponseWriter,
148 r *http.Request,
149 org orgsdb.Org,
150 form settingsProfileForm,
151 errMsg string,
152 success string,
153 ) {
154 _ = h.d.Render.RenderPage(w, r, "orgs/settings_profile", map[string]any{
155 "Title": org.Slug + " - profile settings",
156 "CSRFToken": middleware.CSRFTokenForRequest(r),
157 "Org": org,
158 "Form": form,
159 "AvatarURL": "/avatars/" + url.PathEscape(org.Slug),
160 "ActiveOrgNav": "settings",
161 "OrgSettingsActive": "profile",
162 "BillingEnabled": h.d.BillingEnabled,
163 "AvatarUploadEnabled": h.d.ObjectStore != nil,
164 "HasAvatar": org.AvatarObjectKey.Valid && org.AvatarObjectKey.String != "",
165 "Error": errMsg,
166 "Success": success,
167 })
168 }
169
170 func validateOrgProfile(f *settingsProfileForm) string {
171 if utf8.RuneCountInString(f.DisplayName) > orgProfileMaxDisplayName {
172 return "Organization display name is too long."
173 }
174 if utf8.RuneCountInString(f.Description) > 350 {
175 return "Description is too long (max 350 characters)."
176 }
177 if utf8.RuneCountInString(f.Location) > orgProfileMaxLocation {
178 return "Location is too long."
179 }
180 if utf8.RuneCountInString(f.Website) > orgProfileMaxWebsite {
181 return "Website URL is too long."
182 }
183 if strings.ContainsAny(f.DisplayName, "\r\n") || strings.ContainsAny(f.Location, "\r\n") {
184 return "Single-line fields cannot contain newlines."
185 }
186 if f.Website != "" {
187 if !strings.Contains(f.Website, "://") {
188 f.Website = "https://" + f.Website
189 }
190 u, err := url.Parse(f.Website)
191 if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
192 return "Website must be an http(s) URL."
193 }
194 }
195 if f.BillingEmail != "" {
196 addr, err := mail.ParseAddress(f.BillingEmail)
197 if err != nil || addr.Address != f.BillingEmail {
198 return "Billing email must be a single valid email address."
199 }
200 }
201 return ""
202 }
203