Commit ec2ae93416

ec2ae93416df10c3876972f8b9d494ba932dac80

parent: aaf2234b85

Verified · cmc ci/build: success ci/test: success

cmc <hello@cleberg.net> · 2026-10-02 15:15 UTC

web: footer links to the operator's abuse and terms pages

[web] abuse_url and terms_url, empty by default, add "Report abuse"
and "Terms" to every page's footer. Wiki pages Abuse and Terms are
gitbay.org's.

Closes #323
Closes #324

Layout: unified · split

.gitbay/wiki/Abuse.org added +21
@@ -0,0 +1,21 @@
1#+title: Reporting abuse
2
3This page is for gitbay.org. Another instance links its own page from
4its footer, or none.
5
6Mail =abuse@gitbay.org= with:
7
8- the URL of the repository, issue, merge request, comment or snippet;
9- what is wrong with it: malware, spam, phishing, a copyright or
10 licence claim, illegal content, harassment, or anything else;
11- if you are the rights holder or the person affected, say so.
12
13You do not need an account. One person runs the instance and reads the
14mail. Depending on the report, the content is removed, the repository
15is archived or made private, or the account is disabled
16(=admin user disable=, which refuses SSH, web and API access without
17deleting anything). The account holder is told what was done and why,
18unless that would help them continue.
19
20A security problem in gitbay itself can go to the same address; do not
21file it as a public issue.
.gitbay/wiki/Admin.org +3
@@ -115,6 +115,9 @@ build page's live log arrives only when the build ends;
115 gitbay itself. 115 gitbay itself.
116- =privacy_notice= — operator text shown on =/privacy= under the fixed 116- =privacy_notice= — operator text shown on =/privacy= under the fixed
117 statement. 117 statement.
118- =abuse_url=, =terms_url= (empty) — linked from every page's footer as
119 "Report abuse" and "Terms"; no link when empty. gitbay.org points them
120 at the [[Abuse][Abuse]] and [[Terms][Terms]] wiki pages.
118- =apple_app_ids= (empty) — iOS app IDs (=TEAMID.bundle.id=) allowed to 121- =apple_app_ids= (empty) — iOS app IDs (=TEAMID.bundle.id=) allowed to
119 open this instance's links, served at 122 open this instance's links, served at
120 =/.well-known/apple-app-site-association=. Empty leaves the path a 123 =/.well-known/apple-app-site-association=. Empty leaves the path a
.gitbay/wiki/Terms.org added +51
@@ -0,0 +1,51 @@
1#+title: Terms
2
3These are the terms for gitbay.org, kept informal on purpose. Another
4instance sets its own.
5
6* What this is
7
8One person runs gitbay.org on one server in Nuremberg, Germany. It is
9free, comes with no warranty, and makes no uptime promise. Repositories
10are backed up nightly off the host, but keep your own clone of anything
11that matters.
12
13* What is not allowed
14
15- malware, phishing kits, or anything built to attack other systems
16 (security research and proof-of-concept code with a clear purpose are
17 fine);
18- spam, including accounts or repositories that exist to carry links;
19- content that is illegal in Germany, or that infringes someone's
20 copyright;
21- harassment of other users;
22- using CI, storage or bandwidth as general-purpose hosting: crypto
23 mining, file dumps, or mirrors of large binaries unrelated to a
24 project.
25
26* Limits
27
28Each account may own 250 repositories and 5 GB. An organization has
29the same caps, and an account may create 5 organizations. Ask if you
30need more.
31
32* What the operator may do
33
34Remove content, archive a repository or make it private, or disable an
35account, when it breaks the rules above or puts the instance at risk.
36Reports go to the address on the [[Abuse][abuse page]].
37
38* Leaving
39
40=account export= writes your profile, repositories, issues and merge
41requests as a bundle another gitbay instance can replay
42(=gitbay migrate=), and every repository can be cloned. To have
43the account deleted, mail the abuse address from the account's primary
44email.
45
46If gitbay.org ever shuts down, every account with a verified address
47gets notice and time to export first.
48
49* Privacy
50
51See https://gitbay.org/privacy.
internal/config/config.go +4
@@ -130,6 +130,10 @@ type Web struct {
130 // PrivacyNotice is operator-provided text shown on /privacy under the 130 // PrivacyNotice is operator-provided text shown on /privacy under the
131 // fixed project-level statement. Plain text; blank paragraphs split. 131 // fixed project-level statement. Plain text; blank paragraphs split.
132 PrivacyNotice string `toml:"privacy_notice"` 132 PrivacyNotice string `toml:"privacy_notice"`
133 // AbuseURL and TermsURL, when set, are linked from every page's
134 // footer: where to report abuse, and the instance's terms of use.
135 AbuseURL string `toml:"abuse_url"`
136 TermsURL string `toml:"terms_url"`
133 // AppleAppIDs are the iOS apps (TEAMID.bundle.id) that may open this 137 // AppleAppIDs are the iOS apps (TEAMID.bundle.id) that may open this
134 // instance's links, served in /.well-known/apple-app-site-association. 138 // instance's links, served in /.well-known/apple-app-site-association.
135 // Empty leaves the route unregistered. 139 // Empty leaves the route unregistered.
internal/httpd/accounts.go +3 −3
@@ -90,7 +90,7 @@ func (s *Server) renderLogin(w http.ResponseWriter, errMsg string, sent bool, ne
90 EmailLogin bool 90 EmailLogin bool
91 Sent bool 91 Sent bool
92 Next string 92 Next string
93 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, 93 }{s.anonBase(),
94 s.cfg.Registration.Mode, errMsg, s.emailLoginEnabled(), sent, next}) 94 s.cfg.Registration.Mode, errMsg, s.emailLoginEnabled(), sent, next})
95} 95}
96 96
@@ -399,7 +399,7 @@ func (s *Server) renderSignup(w http.ResponseWriter, errMsg, username string) {
399 Mode string // open | invite 399 Mode string // open | invite
400 Error string 400 Error string
401 Username string 401 Username string
402 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, s.cfg.SiteHost(), s.cfg.Registration.Mode, errMsg, username}) 402 }{s.anonBase(), s.cfg.SiteHost(), s.cfg.Registration.Mode, errMsg, username})
403} 403}
404 404
405func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) { 405func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) {
@@ -421,7 +421,7 @@ func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) {
421 Username string 421 Username string
422 Message string 422 Message string
423 Host string 423 Host string
424 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, username, msg, s.cfg.SiteHost()}) 424 }{s.anonBase(), username, msg, s.cfg.SiteHost()})
425} 425}
426 426
427// issueNewPage is what the new-issue form renders with, whether that is a 427// issueNewPage is what the new-issue form renders with, whether that is a
internal/httpd/footer_test.go added +26
@@ -0,0 +1,26 @@
1package httpd
2
3import (
4 "strings"
5 "testing"
6)
7
8// The footer links the operator's terms and abuse pages only when
9// configured, on a page with no viewer and on the 404 page.
10func TestFooterOperatorLinks(t *testing.T) {
11 s, _, _ := newTokenTestServer(t)
12 for _, path := range []string{"/privacy", "/no-such-owner"} {
13 if body := get(t, s.Handler(), path, nil).Body.String(); strings.Contains(body, "Report abuse") || strings.Contains(body, ">Terms<") {
14 t.Errorf("%s: footer links with nothing configured", path)
15 }
16 }
17 s.cfg.Web.AbuseURL = "https://example.test/wiki/Abuse"
18 s.cfg.Web.TermsURL = "https://example.test/wiki/Terms"
19 for _, path := range []string{"/privacy", "/no-such-owner"} {
20 body := get(t, s.Handler(), path, nil).Body.String()
21 if !strings.Contains(body, `<a href="https://example.test/wiki/Abuse">Report abuse</a>`) ||
22 !strings.Contains(body, `<a href="https://example.test/wiki/Terms">Terms</a>`) {
23 t.Errorf("%s: footer lacks the configured links", path)
24 }
25 }
26}
internal/httpd/page.go +11 −2
@@ -26,13 +26,22 @@ type basePage struct {
26 // Theme is stamped on <html> as data-theme: light or dark when the 26 // Theme is stamped on <html> as data-theme: light or dark when the
27 // viewer chose one, empty when the browser's own scheme decides. 27 // viewer chose one, empty when the browser's own scheme decides.
28 Theme string 28 Theme string
29 // AbuseURL and TermsURL are the operator's footer links, empty when
30 // not configured.
31 AbuseURL string
32 TermsURL string
33}
34
35// anonBase is the layout-wide data with no viewer.
36func (s *Server) anonBase() basePage {
37 return basePage{Site: s.siteName(), Host: s.cfg.SiteHost(), AbuseURL: s.cfg.Web.AbuseURL, TermsURL: s.cfg.Web.TermsURL}
29} 38}
30 39
31// base builds the layout-wide data for a request that has not already 40// base builds the layout-wide data for a request that has not already
32// resolved a viewer. 41// resolved a viewer.
33func (s *Server) base(r *http.Request) basePage { 42func (s *Server) base(r *http.Request) basePage {
34 if s.cfg.Web.Mode != "accounts" { 43 if s.cfg.Web.Mode != "accounts" {
35 return basePage{Site: s.siteName(), Host: s.cfg.SiteHost()} 44 return s.anonBase()
36 } 45 }
37 return s.baseFor(s.viewer(r)) 46 return s.baseFor(s.viewer(r))
38} 47}
@@ -40,7 +49,7 @@ func (s *Server) base(r *http.Request) basePage {
40// baseFor is base for a handler that already holds the viewer, so the 49// baseFor is base for a handler that already holds the viewer, so the
41// session lookup is not repeated. 50// session lookup is not repeated.
42func (s *Server) baseFor(viewer store.User) basePage { 51func (s *Server) baseFor(viewer store.User) basePage {
43 b := basePage{Site: s.siteName(), Host: s.cfg.SiteHost()} 52 b := s.anonBase()
44 if viewer.ID == 0 { 53 if viewer.ID == 0 {
45 return b 54 return b
46 } 55 }
internal/httpd/web.go +1 −1
@@ -198,7 +198,7 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
198 Accounts bool 198 Accounts bool
199 Signup bool 199 Signup bool
200 EmailLogin bool 200 EmailLogin bool
201 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, host, s.cfg.Web.Mode == "accounts", 201 }{s.anonBase(), host, s.cfg.Web.Mode == "accounts",
202 s.cfg.Web.Mode == "accounts" && s.cfg.Registration.Mode != "closed", 202 s.cfg.Web.Mode == "accounts" && s.cfg.Registration.Mode != "closed",
203 s.emailLoginEnabled()}) 203 s.emailLoginEnabled()})
204} 204}
internal/web/templates/layout.html +1 −1
@@ -88,7 +88,7 @@
88</main> 88</main>
89 89
90<footer> 90<footer>
91 <p>Powered by <a href="https://gitbay.org/krz/gitbay">gitbay</a>{{with gitbayVersion}} · <code><a href="https://gitbay.org/krz/gitbay/commit/{{gitbayCommit}}">{{.}}</a></code>{{end}} · <a href="https://apps.apple.com/us/app/gitbay/id6806399095">iOS app</a> · <a href="/privacy">Privacy</a></p> 91 <p>Powered by <a href="https://gitbay.org/krz/gitbay">gitbay</a>{{with gitbayVersion}} · <code><a href="https://gitbay.org/krz/gitbay/commit/{{gitbayCommit}}">{{.}}</a></code>{{end}} · <a href="https://apps.apple.com/us/app/gitbay/id6806399095">iOS app</a> · <a href="/privacy">Privacy</a>{{with field . "TermsURL"}} · <a href="{{.}}">Terms</a>{{end}}{{with field . "AbuseURL"}} · <a href="{{.}}">Report abuse</a>{{end}}</p>
92</footer> 92</footer>
93</div> 93</div>
94</div> 94</div>