fierj
PublicTiny personal git forge
9b9a258635d47eeb732061f1d2a48ced12e5b45f
diff --git a/handlers.go b/handlers.go
index 85044e5..917915b 100644
--- a/handlers.go
+++ b/handlers.go
@@ -105,6 +105,16 @@ func withUser(r *http.Request, data map[string]any) map[string]any {
return data
}
+// checkRepoAccess returns true if the current user can view this repo.
+// Private repos require login. Writes 404 or redirects to login if denied.
+func checkRepoAccess(w http.ResponseWriter, r *http.Request, git *Git) bool {
+ if git.IsPrivate() && User(r) == "" {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return false
+ }
+ return true
+}
+
func Repos(cfg Config, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
repos, err := ListRepos(cfg)
@@ -112,9 +122,17 @@ func Repos(cfg Config, tmpl *template.Template) http.HandlerFunc {
renderError(w, tmpl, http.StatusInternalServerError, "failed to list repositories: "+err.Error())
return
}
+ // Filter private repos for anonymous users.
+ user := User(r)
+ visible := make([]*Git, 0, len(repos))
+ for _, repo := range repos {
+ if !repo.IsPrivate() || user != "" {
+ visible = append(visible, repo)
+ }
+ }
tmpl.ExecuteTemplate(w, "repos.html", map[string]any{
- "Repos": repos,
- "User": User(r),
+ "Repos": visible,
+ "User": user,
})
}
}
@@ -126,6 +144,9 @@ func Tree(cfg Config, tmpl *template.Template) http.HandlerFunc {
LastCommit *Commit
}
git := &Git{Dir: cfg.Dir, Name: r.PathValue("repo")}
+ if !checkRepoAccess(w, r, git) {
+ return
+ }
treePath := r.PathValue("path")
ref := r.PathValue("ref")
if ref == "" {
@@ -202,6 +223,9 @@ func Tree(cfg Config, tmpl *template.Template) http.HandlerFunc {
func Blob(cfg Config, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
git := &Git{Dir: cfg.Dir, Name: r.PathValue("repo")}
+ if !checkRepoAccess(w, r, git) {
+ return
+ }
filePath := r.PathValue("path")
ref := r.PathValue("ref")
if ref == "" {
@@ -231,6 +255,9 @@ func Blob(cfg Config, tmpl *template.Template) http.HandlerFunc {
func Log(cfg Config, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
git := &Git{Dir: cfg.Dir, Name: r.PathValue("repo")}
+ if !checkRepoAccess(w, r, git) {
+ return
+ }
ref := r.PathValue("ref")
commits, err := git.Log(ref, 50)
if err != nil {
@@ -255,6 +282,9 @@ func Log(cfg Config, tmpl *template.Template) http.HandlerFunc {
func Diff(cfg Config, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
git := &Git{Dir: cfg.Dir, Name: r.PathValue("repo")}
+ if !checkRepoAccess(w, r, git) {
+ return
+ }
ref := r.PathValue("ref")
commit, err := git.Diff(ref)
if err != nil || len(commit) == 0 {
@@ -322,6 +352,9 @@ func NewRepoPost(cfg Config, tmpl *template.Template) http.HandlerFunc {
func Refs(cfg Config, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
git := &Git{Dir: cfg.Dir, Name: r.PathValue("repo")}
+ if !checkRepoAccess(w, r, git) {
+ return
+ }
ref := git.DefaultBranch()
tmpl.ExecuteTemplate(w, "refs.html", map[string]any{
"Repo": git.Repo(),
@@ -334,3 +367,71 @@ func Refs(cfg Config, tmpl *template.Template) http.HandlerFunc {
})
}
}
+
+func SettingsGet(cfg Config, tmpl *template.Template) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if User(r) == "" {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ git := &Git{Dir: cfg.Dir, Name: r.PathValue("repo")}
+ meta := git.LoadMeta()
+ tmpl.ExecuteTemplate(w, "settings.html", map[string]any{
+ "Repo": git.Repo(),
+ "Ref": git.DefaultBranch(),
+ "Description": meta.Description,
+ "IsPrivate": meta.IsPrivate,
+ "AuthorizedKeys": strings.Join(meta.AuthorizedKeys, "\n"),
+ "ProtectedBranches": strings.Join(meta.ProtectedBranches, "\n"),
+ "ActiveTab": "settings",
+ "User": User(r),
+ })
+ }
+}
+
+func SettingsPost(cfg Config, tmpl *template.Template) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if User(r) == "" {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ git := &Git{Dir: cfg.Dir, Name: r.PathValue("repo")}
+ meta := git.LoadMeta()
+ meta.Description = strings.TrimSpace(r.FormValue("description"))
+ meta.IsPrivate = r.FormValue("is_private") == "true"
+
+ // Parse authorized keys: one key per line, skip empty lines
+ rawKeys := strings.TrimSpace(r.FormValue("authorized_keys"))
+ if rawKeys != "" {
+ meta.AuthorizedKeys = nil
+ for _, k := range strings.Split(rawKeys, "\n") {
+ k = strings.TrimSpace(k)
+ if k != "" {
+ meta.AuthorizedKeys = append(meta.AuthorizedKeys, k)
+ }
+ }
+ } else {
+ meta.AuthorizedKeys = nil
+ }
+
+ // Parse protected branches
+ rawBranches := strings.TrimSpace(r.FormValue("protected_branches"))
+ if rawBranches != "" {
+ meta.ProtectedBranches = nil
+ for _, b := range strings.Split(rawBranches, "\n") {
+ b = strings.TrimSpace(b)
+ if b != "" {
+ meta.ProtectedBranches = append(meta.ProtectedBranches, b)
+ }
+ }
+ } else {
+ meta.ProtectedBranches = nil
+ }
+
+ if err := git.SaveMeta(meta); err != nil {
+ renderError(w, tmpl, http.StatusInternalServerError, "failed to save settings: "+err.Error())
+ return
+ }
+ http.Redirect(w, r, "/"+git.Name+"/settings", http.StatusSeeOther)
+ }
+}
diff --git a/main.go b/main.go
index 652b902..62393c2 100644
--- a/main.go
+++ b/main.go
@@ -64,6 +64,8 @@ func main() {
mux.HandleFunc("GET /{repo}/log/{ref}", Log(cfg, tmpl))
mux.HandleFunc("GET /{repo}/commit/{ref}", Diff(cfg, tmpl))
mux.HandleFunc("GET /{repo}/refs/", Refs(cfg, tmpl))
+ mux.HandleFunc("GET /{repo}/settings", SettingsGet(cfg, tmpl))
+ mux.HandleFunc("POST /{repo}/settings", SettingsPost(cfg, tmpl))
// Wrap with auth middleware and setup redirect.
var handler http.Handler = mux
diff --git a/templates/repos.html b/templates/repos.html
index debe97e..5816688 100644
--- a/templates/repos.html
+++ b/templates/repos.html
@@ -12,8 +12,7 @@
d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z" />
</svg>
<a href="/{{.Name}}" style="font-weight: 600; font-size: 1rem;">{{.Name}}</a>
- <span
- style="font-size: 0.7rem; border: 1px solid var(--border); border-radius: 2em; padding: 0.1rem 0.5rem; color: var(--text-secondary);">Public</span>
+ <span style="font-size: 0.7rem; border: 1px solid var(--border); border-radius: 2em; padding: 0.1rem 0.5rem; color: var(--text-secondary);">{{if .IsPrivate}}Private{{else}}Public{{end}}</span>
</div>
{{if .Description}}<p style="color: var(--text-secondary); margin-top: var(--space-xs); font-size: 0.85rem;">
{{.Description}}</p>{{end}}
diff --git a/templates/settings.html b/templates/settings.html
new file mode 100644
index 0000000..298e97d
--- /dev/null
+++ b/templates/settings.html
@@ -0,0 +1,39 @@
+{{template "head" .}}
+{{define "title"}}settings - {{.Repo}} - fierj{{end}}
+{{template "repo-header" .}}
+{{template "repo-tabs" .}}
+
+<h1 style="margin-bottom:var(--space-lg);font-size:1.25rem;">Repository settings</h1>
+
+<form method="post" style="max-width:600px;">
+ <div class="form-group">
+ <label for="description">Description</label>
+ <input type="text" id="description" name="description" value="{{.Description}}">
+ </div>
+
+ <div class="form-group">
+ <label>
+ <input type="checkbox" name="is_private" value="true" {{if .IsPrivate}}checked{{end}}>
+ Private repository (only visible to logged-in users)
+ </label>
+ </div>
+
+ <div class="form-group">
+ <label for="authorized_keys">Authorized SSH keys (one per line)</label>
+ <textarea id="authorized_keys" name="authorized_keys" rows="6" style="font-family:var(--font-mono);font-size:0.8rem;">{{.AuthorizedKeys}}</textarea>
+ <div style="font-size:0.8rem;color:var(--text-secondary);margin-top:var(--space-xs);">
+ Users with these keys can push to this repo. Leave empty to allow anyone with SSH access.
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label for="protected_branches">Protected branches (one per line)</label>
+ <textarea id="protected_branches" name="protected_branches" rows="4" placeholder="main release/*" style="font-family:var(--font-mono);font-size:0.8rem;">{{.ProtectedBranches}}</textarea>
+ <div style="font-size:0.8rem;color:var(--text-secondary);margin-top:var(--space-xs);">
+ Branches that require authorized keys to push. Supports wildcards (e.g. <code>release/*</code>).
+ </div>
+ </div>
+
+ <button type="submit" class="btn">Save settings</button>
+</form>
+{{template "foot" .}}