New Sign in

fierj

Public

Tiny 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&#10;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" .}}