Sarah@auth.lopeos.org submitted a patch request Feb 16, 2025 21:13
- Title: Styling and LimitConfig
- Author: Sarah Jamie Lewis <sarah@openprivacy.ca>
README.md
@@ -3,7 +3,7 @@
## Configuration
-The directory you run senary from should a copy of the `templates` directory in this repository, and also a `senary.json` file like below:
+The directory you run senary from should contain a copy of the `templates` directory from this repository, and also a `senary.json` file like below:
```
{
common/config.go
@@ -9,7 +9,14 @@ import (
"slices"
)
+type LimitsConfig struct {
+ MaxPatchSizeMB int
+ MaxCommentLength int
+}
+
type Config struct {
+ LimitsConfig
+
BaseDir string // the directory where static html/css files are stored
RepoDir string // the directory where static repo html files will be stored
RequestsDir string // the directory where saved change requests will be stored
@@ -45,6 +52,15 @@ func LoadConfig(path string) (*Config, error) {
if err != nil {
return nil, fmt.Errorf("could not parse template files: %v", err)
}
+
+ if config.MaxPatchSizeMB == 0 {
+ config.MaxPatchSizeMB = 20
+ }
+
+ if config.MaxCommentLength == 0 {
+ config.MaxCommentLength = 8192
+ }
+
config.Templates = templates
config.RepoPathMap = make(map[string]string)
for _, base := range config.RepoBases {
gen/generate.go
@@ -98,7 +98,7 @@ func GenerateRepoPages(config *common.Config, repo string) error {
langLexer = lexers.Fallback
}
}
- style := styles.Get("dracula")
+ style := styles.Get("tokyonight-moon")
if style == nil {
style = styles.Fallback
}
repo/repo.go
@@ -5,6 +5,7 @@ import (
"html/template"
"net/http"
"os"
+ "path"
"path/filepath"
"senary/auth"
"senary/common"
@@ -23,10 +24,18 @@ func NewRepository(config *common.Config, repo string, ac *auth.AuthClient) *Rep
return &Repository{config: config, repo: repo, ac: ac}
}
+type KeyVal struct {
+ Key string
+ Val string
+}
+
type StaticDir struct {
RequestBase
- Table template.HTML
- DirName string
+ Table template.HTML
+ Title string
+ Filename string
+ Breadcrumbs []KeyVal
+ DirName string
}
func (repo *Repository) ServeTree(w http.ResponseWriter, r *http.Request) {
@@ -56,9 +65,37 @@ func (repo *Repository) ServeTree(w http.ResponseWriter, r *http.Request) {
}
user := repo.ac.GetAuthInfo(repo.config, r)
+ title, _ := strings.CutSuffix(localFile, ".html")
+ breadcrumbs := strings.Split(title, "/")
+ var breadcrumbsMap []KeyVal
+ filename := filepath.Base(title)
+ if !strings.HasSuffix(localFile, "/index.html") {
+ breadcrumbsMap = make([]KeyVal, len(breadcrumbs)-2)
+ soafar := path.Join("/repos", repo.repo)
+ for i, bc := range breadcrumbs[1 : len(breadcrumbs)-1] {
+ soafar = path.Join(soafar, bc)
+ breadcrumbsMap[i].Key = bc
+ breadcrumbsMap[i].Val = soafar
+ }
+ } else {
+ breadcrumbs := strings.Split(filepath.Dir(title), "/")
+ if len(breadcrumbs) > 2 {
+ breadcrumbsMap = make([]KeyVal, len(breadcrumbs)-2)
+ soafar := path.Join("/repos", repo.repo)
+ for i, bc := range breadcrumbs[1 : len(breadcrumbs)-1] {
+ soafar = path.Join(soafar, bc)
+ breadcrumbsMap[i].Key = bc
+ breadcrumbsMap[i].Val = soafar
+ }
+ }
- err = repo.config.Templates.ExecuteTemplate(w, "dir.tpl.html", StaticDir{Table: template.HTML(data), DirName: filepath.Dir(localFile),
+ filename = filepath.Base(filepath.Dir(title))
+ }
+ err = repo.config.Templates.ExecuteTemplate(w, "dir.tpl.html", StaticDir{Table: template.HTML(data), DirName: filepath.Dir(localFile),
+ Title: filename,
+ Breadcrumbs: breadcrumbsMap,
+ Filename: filename,
RequestBase: RequestBase{
Repo: repo.repo, User: user, ClientID: repo.config.ClientID}})
if err != nil {
repo/requests.go
@@ -385,12 +385,7 @@ func (rm *RequestManager) updateLog(logpath string, hash string, status common.L
func (rm *RequestManager) handlePatch(w http.ResponseWriter, r *http.Request) {
- // Parse our multipart form, 10 << 20 specifies a maximum
- // upload of 10 MB files.
- r.ParseMultipartForm(1024 * 1024 * 200)
- // FormFile returns the first file for the given key `myFile`
- // it also returns the FileHeader so we can get the Filename,
- // the Header and the size of the file
+ r.ParseMultipartForm(1024 * 1024 * int64(rm.config.MaxPatchSizeMB))
file, _, err := r.FormFile("patchfile")
if err != nil {
rm.errorHandler("Could Not Create Patch Request", w, r)
@@ -447,21 +442,21 @@ func (rm *RequestManager) SaveIssue(issueRequest *common.IssueRequest, w http.Re
os.MkdirAll(filepath.Dir(requestName), 0755)
err := os.WriteFile(requestName, issueRequest.Serialize(), 0644)
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
// create the parent entry of the requests log
err = rm.updateLog(filepath.Join(rm.config.RequestsDir, rm.repo, issueRequest.ID, "log"), issueRequest.ID, common.LOG_CREATED)
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
// finally write to our main requests log
err = rm.updateLog(filepath.Join(rm.config.RequestsDir, rm.repo, "log"), issueRequest.ID, common.LOG_CREATED)
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
@@ -469,14 +464,14 @@ func (rm *RequestManager) SaveIssue(issueRequest *common.IssueRequest, w http.Re
// Auth Approve Maintainer Created Issues
err = rm.updateLog(filepath.Join(rm.config.RequestsDir, rm.repo, "log"), issueRequest.ID, common.LOG_APPROVED)
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
// Auto Approve Maintainer Top Level Issue
err = rm.updateLog(filepath.Join(rm.config.RequestsDir, rm.repo, issueRequest.ID, "log"), issueRequest.ID, common.LOG_APPROVED)
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
@@ -498,14 +493,19 @@ func (rm *RequestManager) handleNew(w http.ResponseWriter, r *http.Request) {
if replyto == "" {
if newtype == "issue" {
- if len(summary) == 0 || len(description) == 0 {
- rm.Serve(w, r)
+ if len(summary) == 0 || len(summary) > rm.config.MaxCommentLength {
+ rm.errorHandler(fmt.Errorf("summary was empty, or too long").Error(), w, r)
+ return
+ }
+
+ if len(description) == 0 || len(description) > rm.config.MaxCommentLength {
+ rm.errorHandler(fmt.Errorf("description was empty, or too long").Error(), w, r)
return
}
issueRequest, err := common.NewIssueRequest(summary, description, rm.ac.GetAuthInfo(rm.config, r))
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
@@ -520,13 +520,13 @@ func (rm *RequestManager) handleNew(w http.ResponseWriter, r *http.Request) {
}
} else {
// we are replying to an issue....
- if len(description) == 0 {
- rm.Serve(w, r)
+ if len(description) == 0 || len(description) > rm.config.MaxCommentLength {
+ rm.errorHandler(fmt.Errorf("description was empty, or too long").Error(), w, r)
return
}
issueRequest, err := common.NewIssueRequest(summary, description, rm.ac.GetAuthInfo(rm.config, r))
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
issueRequest.RequestType = common.RequestReply
@@ -534,14 +534,14 @@ func (rm *RequestManager) handleNew(w http.ResponseWriter, r *http.Request) {
requestName := filepath.Join(rm.config.RequestsDir, rm.repo, replyto, issueRequest.ID)
err = os.WriteFile(requestName, issueRequest.Serialize(), 0644)
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
// create the parent entry of the requests log
err = rm.updateLog(rm.RequestIssueLogPath(replyto), issueRequest.ID, common.LOG_CREATED)
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
@@ -549,7 +549,7 @@ func (rm *RequestManager) handleNew(w http.ResponseWriter, r *http.Request) {
// Auth Approve Maintainer Created Issues
err = rm.updateLog(rm.RequestIssueLogPath(replyto), issueRequest.ID, common.LOG_APPROVED)
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ rm.errorHandler(err.Error(), w, r)
return
}
static/css/custom.css
@@ -5,10 +5,6 @@
}
- body {
-
- }
-
pre.code:before {
counter-reset: listing;
@@ -43,6 +39,10 @@
grid-row-gap: 0px;
}
+ .container-fluid {
+ padding-left:2%;
+ padding-right:2%;
+ }
.shrink {
flex: unset;
@@ -95,6 +95,10 @@ pre > code{
padding:0px;
}
+code {
+ color:var(--pico-color-light);
+}
+
td {
line-height: normal;
}
@@ -103,4 +107,5 @@ td {
.listing {
width:100%;
overflow:scroll;
-}
+}
+
templates/auth.success.tpl.html
@@ -3,10 +3,10 @@
<title>Senary</title>
</head>
<body>
- <header class="container">
+ <header class="container-fluid">
{{ template "header.tpl.html" }}
</header>
-<main class="container repomain">
+<main class="container-fluid repomain">
<h1>Success! Welcome {{.}}</h1>
templates/dir.tpl.html
@@ -3,14 +3,28 @@
<title>{{.DirName}} - Senary</title>
</head>
<body>
- <header class="container">
+ <header class="container-fluid">
{{ template "header.tpl.html" .}}
</header>
-<main class="container repomain">
+<main class="container-fluid repomain">
{{template "repomenu.tpl.html" .}}
+<div>
+ {{ $length := len .Breadcrumbs }} {{ if ne $length 0 }}
+ <nav aria-label="breadcrumb">
+ <ul>
+
+ {{range .Breadcrumbs}}
+ <li><a href="{{.Val}}">{{.Key}}</a></li>
+ {{end}}
+ <li>{{.Filename}}</li>
+ </ul>
+ </nav>
+ {{end}}
+<h2>{{.Title}}</h2>
{{.Table}}
+</div>
</main>
<footer>
</footer>
templates/index.tpl.html
@@ -1,11 +1,52 @@
{{template "prelude.tpl.html" .}}
-<title>Repositories - Senary</title>
+<title>Senary - Lightweight Software Forge</title>
</head>
<body>
-<header class="container">
+<header class="container-fluid">
{{ template "header.tpl.html" .}}
</header>
-<main class="container">
+<main class="container-fluid repomain">
+
+
+ <aside>
+
+
+ </aside>
+
+ <div>
+ <details open>
+ <summary><h2>What is Senary?</h2></summary>
+ <p><a href="/repos/senary">Senary</a> is a prototype, lightweight software forge designed for self-hosting. Senary supports <a href="https://indieauth.net/">IndieAuth</a> identification and authorization of contributors and maintainers. </p>
+ </details>
+ <br/>
+
+ <details open>
+ <summary><h2>What Features does Senary Have?</h2></summary>
+ <p>Senary supports:</p>
+ <ul>
+ <li><strong>Patch-based Workflow</strong> - rather than mimicking proprietary pull request flows, Senary is based on the open sharing and application of patches.</li>
+ <li><strong>Moderated change requests (issues / new patches)</strong> - to limit to impact and utility of spam, all requests are hidden from public listing until approved by a maintainer.</li>
+ <li><strong>Permissionless contributions</strong> - Senary allows users to submit bugs and fixes without needing an account, or even an IndieAuth server. Senary only keeps track of maintainer permissions for the purpose of moderating requests, and applying patches.</li>
+ </ul>
+ </details>
+
+ <br/>
+ <details open>
+ <summary><h2>What is IndieAuth?</h2></summary>
+ <p><a href="https://indieauth.net/">IndieAuth</a> is a decentralized identity protocol built on top of OAuth 2.0.</p>
+
+ <p>This allows individual websites like someone's WordPress, Mastodon, or Gitea server to become its own identity provider, and can be used to sign in to other instances. Both users and applications are identified by URLs, avoiding the need for getting API keys or making new accounts.</p>
+ </details>
+
+ <br/>
+
+ <details open>
+ <summary><h2>What is the current status of the Senary project?</h2></summary>
+ <p>Senary is an actively developed prototype and subject to frequent changes. We encourage people to test it out and report issues/fixes.
+ </p>
+ </details>
+
+ </div>
</main>
<footer>
templates/login.tpl.html
@@ -3,10 +3,10 @@
<title>Authenticate with IndieAuth - Senary</title>
</head>
<body>
- <header class="container">
+ <header class="container-fluid">
{{ template "header.tpl.html" .}}
</header>
-<main class="container repomain">
+<main class="container-fluid repomain">
<form action="/login" method="post">
<input type="text" name="profile" placeholder="yourdomain.com" required />
templates/repos.tpl.html
@@ -3,17 +3,17 @@
<title>Repositories - Senary</title>
</head>
<body>
-<header class="container">
+<header class="container-fluid">
{{ template "header.tpl.html" .}}
</header>
-<main class="container">
+<main class="container-fluid">
<table>
-<tr><th>File</th><th>Commit Message</th><th>Date</th></tr>
+<tr><th>Repository</th></tr>
{{range .Contents}}
{{if .Dir}}
- <tr><td><a href="./{{.Name}}">{{.Name}}</a></td><td>{{.Message}}</td><td>{{.Date}}</td></tr>
+ <tr><td><a href="./{{.Name}}">{{.Name}}</a></td></tr>
{{else}}
- <tr><td><a href="./{{.Name}}.html">{{.Name}}</a></td><td>{{.Message}}</td><td>{{.Date}}</td></tr>
+ <tr><td><a href="./{{.Name}}.html">{{.Name}}</a></td></tr>
{{end}}
{{end}}
</main>
templates/request.list.tpl.html
@@ -2,10 +2,10 @@
<title>{{.Repo}} Change Requests - Senary</title>
</head>
<body>
- <header class="container">
+ <header class="container-fluid">
{{ template "header.tpl.html" .}}
</header>
-<main class="container repomain">
+<main class="container-fluid repomain">
{{template "repomenu.tpl.html" .}}
<div>
templates/request.new.tpl.html
@@ -2,15 +2,17 @@
<title>{{.Repo}} New Change Request - Senary</title>
</head>
<body>
- <header class="container">
+ <header class="container-fluid">
{{ template "header.tpl.html" .}}
</header>
-<main class="container repomain">
+<main class="container-fluid repomain">
{{template "repomenu.tpl.html" .}}
<div>
- {{range .Warnings}}
+ {{range .Warnings}}
+ <div class="error">{{.Message}}</div>
+ <br/>
{{end}}
<h2>Submit a New Change Request</h2>
<form method="post" >
templates/request.newpatch.tpl.html
@@ -3,10 +3,10 @@
<title>{{.Repo}} New Change Request - Senary</title>
</head>
<body>
- <header class="container">
+ <header class="container-fluid">
{{ template "header.tpl.html" .}}
</header>
-<main class="container repomain">
+<main class="container-fluid repomain">
{{template "repomenu.tpl.html" .}}
<div>
templates/request.thread.tpl.html
@@ -2,10 +2,10 @@
<title>{{.Repo}} Change Request - {{.Title}} - Senary</title>
</head>
<body>
- <header class="container">
+ <header class="container-fluid">
{{ template "header.tpl.html" .}}
</header>
-<main class="container repomain">
+<main class="container-fluid repomain">
{{template "repomenu.tpl.html" .}}
<div>