Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-07-21 02:52:36
Commita0ba6ae56e9aa32404aeaa74ab9792b3525eb81b
Parent4a160dd13c06c4a572a55de208200f686a771eaf

Implement admin repository editing

Diffstat

M main.go | 5 ++---
A res/admin/repo_edit.html | 22 ++++++++++++++++++++++
A res/base/header.html | 23 +++++++++++++++++++++++
M res/repo/header.html | 6 +++---
R res/repo_index.html -> res/index.html | 1 +
M res/res.go | 12 +++++++++---
M src/admin.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
M src/auth.go | 12 ++++++++++++
M src/git.go | 2 +-
M src/http.go | 8 +++++---
M src/repo.go | 70 +++++++++++++++++++++++++++++++++++++++++++++-------------------------

11 files changed, 195 insertions, 50 deletions

diff --git a/main.go b/main.go
index 1bb34eb..f3db37f 100644
--- a/main.go
+++ b/main.go
@@ -26,14 +26,13 @@ func main() {
 	h.Path("/").HandlerFunc(goit.HandleIndex)
 	h.Path("/user/login").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogin)
 	h.Path("/user/logout").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogout)
-	// h.Path("/user/settings").Methods("GET").HandlerFunc()
 	h.Path("/repo/create").Methods("GET", "POST").HandlerFunc(goit.HandleRepoCreate)
-	// h.Path("/repo/delete").Methods("POST").HandlerFunc()
-	// h.Path("/admin/settings").Methods("GET").HandlerFunc()
+	// h.Path("/admin").Methods("GET").HandlerFunc()
 	h.Path("/admin/users").Methods("GET").HandlerFunc(goit.HandleAdminUsers)
 	h.Path("/admin/user/create").Methods("GET", "POST").HandlerFunc(goit.HandleAdminUserCreate)
 	h.Path("/admin/user/edit").Methods("GET", "POST").HandlerFunc(goit.HandleAdminUserEdit)
 	h.Path("/admin/repos").Methods("GET").HandlerFunc(goit.HandleAdminRepos)
+	h.Path("/admin/repo/edit").Methods("GET", "POST").HandlerFunc(goit.HandleAdminRepoEdit)
 
 	h.Path("/{repo:.+(?:\\.git)$}").Methods(http.MethodGet).HandlerFunc(redirectDotGit)
 	h.Path("/{repo}").Methods(http.MethodGet).HandlerFunc(goit.HandleRepoLog)
diff --git a/res/admin/repo_edit.html b/res/admin/repo_edit.html
new file mode 100644
index 0000000..7556e12
--- /dev/null
+++ b/res/admin/repo_edit.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<head>{{template "base/head" .}}</head>
+<body>
+	<h1>{{.Title}}</h1>
+	<form action="/admin/repo/edit?repo={{.Id}}" method="post">
+		<label for="id">ID:</label>
+		<input type="text" name="id" value="{{.Id}}" disabled><br>
+		<label for="id">Owner:</label>
+		<input type="text" name="owner" value="{{.Owner}}" disabled><br>
+		<label for="reponame">Name:</label>
+		<input type="text" name="reponame" value="{{.Name}}"><br>
+		<label for="description">Description:</label>
+		<input type="text" name="description" value="{{.Description}}"><br>
+		<label for="visibility">Visibility:</label>
+		<select name="visibility">
+			<option value="public">Public</option>
+			<option value="private" {{if .IsPrivate}}selected{{end}}>Private</option>
+		</select><br>
+		<input type="submit" value="Update">
+	</form>
+	<span>{{.Message}}</span>
+</body>
diff --git a/res/base/header.html b/res/base/header.html
new file mode 100644
index 0000000..1645db9
--- /dev/null
+++ b/res/base/header.html
@@ -0,0 +1,23 @@
+{{define "base/header"}}
+<table>
+	<tr>
+		<td rowspan="2">
+			<a href="/"><img style="max-height: 24px;" src="/static/favicon.png"></a>
+		</td>
+		<td><h1>{{.Title}}</h1></td>
+	</tr>
+	<tr>
+		<td>
+			<a href="/">Repositories</a>
+			{{if .Admin}}
+				| <a href="/admin">Admin</a>
+			{{end}}
+			{{if .Auth}}
+				| <a href="/user/logout">Logout</a>
+			{{else}}
+				| <a href="/user/login">Login</a>
+			{{end}}
+		</td>
+	</tr>
+</table>
+{{end}}
diff --git a/res/repo_index.html b/res/index.html
similarity index 91%
rename from res/repo_index.html
rename to res/index.html
index 49e67a2..628c8f6 100644
--- a/res/repo_index.html
+++ b/res/index.html
@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <head>{{template "base/head" .}}</head>
 <body>
+	<header>{{template "base/header" .}}</header><hr>
 	<main>
 		<table>
 			<thead>
diff --git a/res/repo/header.html b/res/repo/header.html
index 3a97944..3d249a1 100644
--- a/res/repo/header.html
+++ b/res/repo/header.html
@@ -1,8 +1,8 @@
 <table>
 	<tr>
-		<td rowspan="2"><a href="/">
-			<img style="max-height: 24px;" src="/static/favicon.png">
-		</a></td>
+		<td rowspan="2">
+			<a href="/"><img style="max-height: 24px;" src="/static/favicon.png"></a>
+		</td>
 		<td><h1>{{.Name}}</h1></td>
 	</tr>
 	<tr>
diff --git a/res/res.go b/res/res.go
index dd66d0b..f8a1f73 100644
--- a/res/res.go
+++ b/res/res.go
@@ -5,9 +5,15 @@ import _ "embed"
 //go:embed error.html
 var Error string
 
+//go:embed index.html
+var Index string
+
 //go:embed base/head.html
 var BaseHead string
 
+//go:embed base/header.html
+var BaseHeader string
+
 //go:embed admin/users.html
 var AdminUsers string
 
@@ -20,12 +26,12 @@ var AdminUserEdit string
 //go:embed admin/repos.html
 var AdminRepos string
 
+//go:embed admin/repo_edit.html
+var AdminRepoEdit string
+
 //go:embed repo/header.html
 var RepoHeader string
 
-//go:embed repo_index.html
-var RepoIndex string
-
 //go:embed user/login.html
 var UserLogin string
 
diff --git a/src/admin.go b/src/admin.go
index 0c6199a..e646691 100644
--- a/src/admin.go
+++ b/src/admin.go
@@ -16,7 +16,7 @@ import (
 )
 
 func HandleAdminUsers(w http.ResponseWriter, r *http.Request) {
-	if !authHttpAdmin(r) {
+	if _, admin, _ := AuthHttpAdmin(r); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -61,7 +61,7 @@ func HandleAdminUsers(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) {
-	if !authHttpAdmin(r) {
+	if _, admin, _ := AuthHttpAdmin(r); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -100,13 +100,13 @@ func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	if err := tmpl.ExecuteTemplate(w, "admin/user_create", data); err != nil {
+	if err := tmpl.ExecuteTemplate(w, "admin/user/create", data); err != nil {
 		log.Println("[/admin/user/create]", err.Error())
 	}
 }
 
 func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) {
-	if !authHttpAdmin(r) {
+	if _, admin, _ := AuthHttpAdmin(r); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -130,7 +130,7 @@ func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) {
 	data := struct {
 		Title, Id, Name, FullName, Message string
 		IsAdmin                            bool
-	}{Title: "Edit User ", Id: fmt.Sprint(user.Id), Name: user.Name, FullName: user.FullName, IsAdmin: user.IsAdmin}
+	}{Title: "Edit User", Id: fmt.Sprint(user.Id), Name: user.Name, FullName: user.FullName, IsAdmin: user.IsAdmin}
 
 	if r.Method == http.MethodPost {
 		data.Name = strings.ToLower(r.FormValue("username"))
@@ -175,13 +175,13 @@ func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	if err := tmpl.ExecuteTemplate(w, "admin/user_edit", data); err != nil {
+	if err := tmpl.ExecuteTemplate(w, "admin/user/edit", data); err != nil {
 		log.Println("[/admin/user/edit]", err.Error())
 	}
 }
 
 func HandleAdminRepos(w http.ResponseWriter, r *http.Request) {
-	if !authHttpAdmin(r) {
+	if _, admin, _ := AuthHttpAdmin(r); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -235,12 +235,72 @@ func HandleAdminRepos(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func authHttpAdmin(r *http.Request) bool {
-	if ok, uid := AuthHttp(r); ok {
-		if user, err := GetUser(uid); err == nil && user.IsAdmin {
-			return true
+func HandleAdminRepoEdit(w http.ResponseWriter, r *http.Request) {
+	if _, admin, _ := AuthHttpAdmin(r); !admin {
+		HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	id, err := strconv.ParseUint(r.URL.Query().Get("repo"), 10, 64)
+	if err != nil {
+		HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	repo, err := GetRepo(id)
+	if err != nil {
+		log.Println("[/admin/repo/edit]", err.Error())
+		HttpError(w, http.StatusInternalServerError)
+		return
+	} else if repo == nil {
+		HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	data := struct {
+		Title, Id, Owner, Name, Description, Message string
+		IsPrivate                                    bool
+	}{
+		Title: "Edit Repository", Id: fmt.Sprint(repo.Id), Name: repo.Name, Description: repo.Description,
+		IsPrivate: repo.IsPrivate,
+	}
+
+	owner, err := GetUser(repo.OwnerId)
+	if err != nil {
+		log.Println("[/admin/repo/edit]", err.Error())
+		data.Owner = fmt.Sprint(repo.OwnerId)
+	} else {
+		data.Owner = owner.Name
+	}
+
+	if r.Method == http.MethodPost {
+		data.Name = r.FormValue("reponame")
+		data.Description = r.FormValue("description")
+		data.IsPrivate = r.FormValue("visibility") == "private"
+
+		if data.Name == "" {
+			data.Message = "Name cannot be empty"
+		} else if util.SliceContains(reserved, data.Name) {
+			data.Message = "Name \"" + data.Name + "\" is reserved"
+		} else if exists, err := RepoExists(data.Name); err != nil {
+			log.Println("[/admin/repo/edit]", err.Error())
+			HttpError(w, http.StatusInternalServerError)
+			return
+		} else if exists && data.Name != repo.Name {
+			data.Message = "Name \"" + data.Name + "\" is taken"
+		} else if _, err := db.Exec(
+			"UPDATE repos SET name = ?, name_lower = ?, description = ?, is_private = ? WHERE id = ?",
+			data.Name, strings.ToLower(data.Name), data.Description, data.IsPrivate, repo.Id,
+		); err != nil {
+			log.Println("[/admin/repo/edit]", err.Error())
+			HttpError(w, http.StatusInternalServerError)
+			return
+		} else {
+			data.Message = "Repository \"" + repo.Name + "\" updated successfully"
 		}
 	}
 
-	return false
+	if err := tmpl.ExecuteTemplate(w, "admin/repo/edit", data); err != nil {
+		log.Println("[/admin/repo/edit]", err.Error())
+	}
 }
diff --git a/src/auth.go b/src/auth.go
index d2fb2c0..5888e02 100644
--- a/src/auth.go
+++ b/src/auth.go
@@ -74,6 +74,18 @@ func AuthHttp(r *http.Request) (bool, uint64) {
 	return false, math.MaxUint64
 }
 
+func AuthHttpAdmin(r *http.Request) (auth bool, admin bool, uid uint64) {
+	if ok, uid := AuthHttp(r); ok {
+		if user, err := GetUser(uid); err == nil && user.IsAdmin {
+			return true, true, uid
+		}
+
+		return true, false, uid
+	}
+
+	return false, false, math.MaxUint64
+}
+
 func SessionCookie(r *http.Request) string {
 	if c := util.Cookie(r, "session"); c != nil {
 		return c.Value
diff --git a/src/git.go b/src/git.go
index c1cb30a..5a2438f 100644
--- a/src/git.go
+++ b/src/git.go
@@ -92,7 +92,7 @@ func gitHttpBase(w http.ResponseWriter, r *http.Request, service string) *Repo {
 	}
 
 	/* Load the repository from the database */
-	repo, err := GetRepoByName(db, reponame)
+	repo, err := GetRepoByName(reponame)
 	if err != nil {
 		log.Println("[Git HTTP]", err.Error())
 		w.WriteHeader(http.StatusInternalServerError)
diff --git a/src/http.go b/src/http.go
index 60f5be0..29e858e 100644
--- a/src/http.go
+++ b/src/http.go
@@ -15,17 +15,19 @@ import (
 var tmpl = template.Must(template.New("error").Parse(res.Error))
 
 func init() {
+	template.Must(tmpl.New("index").Parse(res.Index))
 	template.Must(tmpl.New("base/head").Parse(res.BaseHead))
+	template.Must(tmpl.New("base/header").Parse(res.BaseHeader))
 
 	template.Must(tmpl.New("admin/users").Parse(res.AdminUsers))
-	template.Must(tmpl.New("admin/user_create").Parse(res.AdminUserCreate))
-	template.Must(tmpl.New("admin/user_edit").Parse(res.AdminUserEdit))
+	template.Must(tmpl.New("admin/user/create").Parse(res.AdminUserCreate))
+	template.Must(tmpl.New("admin/user/edit").Parse(res.AdminUserEdit))
 	template.Must(tmpl.New("admin/repos").Parse(res.AdminRepos))
+	template.Must(tmpl.New("admin/repo/edit").Parse(res.AdminRepoEdit))
 
 	template.Must(tmpl.New("base/repo_header").Parse(res.RepoHeader))
 	template.Must(tmpl.New("user_login").Parse(res.UserLogin))
 
-	template.Must(tmpl.New("repo_index").Parse(res.RepoIndex))
 	template.Must(tmpl.New("repo_create").Parse(res.RepoCreate))
 
 	template.Must(tmpl.New("repo_log").Parse(res.RepoLog))
diff --git a/src/repo.go b/src/repo.go
index 0f0614b..eae9078 100644
--- a/src/repo.go
+++ b/src/repo.go
@@ -25,47 +25,51 @@ type Repo struct {
 	Id            uint64
 	OwnerId       uint64
 	Name          string
-	NameLower     string
 	Description   string
 	DefaultBranch string
 	IsPrivate     bool
 }
 
 func HandleIndex(w http.ResponseWriter, r *http.Request) {
-	authOk, uid := AuthHttp(r)
+	auth, admin, uid := AuthHttpAdmin(r)
 
 	if rows, err := db.Query("SELECT id, owner_id, name, description, is_private FROM repos"); err != nil {
-		log.Println("[Index:SELECT]", err.Error())
+		log.Println("[/]", err.Error())
 		HttpError(w, http.StatusInternalServerError)
 	} else {
 		defer rows.Close()
 
 		type row struct{ Name, Description, Owner, Visibility, LastCommit string }
-		repos := []row{}
+		data := struct {
+			Title       string
+			Admin, Auth bool
+			Repos       []row
+		}{Title: "Repositories", Admin: admin, Auth: auth}
 
 		for rows.Next() {
-			r := Repo{}
-
-			if err := rows.Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.IsPrivate); err != nil {
-				log.Println("[Index:SELECT:Scan]", err.Error())
-			} else if !r.IsPrivate || (authOk && uid == r.OwnerId) {
-				owner, err := GetUser(r.OwnerId)
+			d := Repo{}
+			if err := rows.Scan(&d.Id, &d.OwnerId, &d.Name, &d.Description, &d.IsPrivate); err != nil {
+				log.Println("[/]", err.Error())
+			} else if !d.IsPrivate || (auth && uid == d.OwnerId) {
+				owner, err := GetUser(d.OwnerId)
 				if err != nil {
-					log.Println("[Index:SELECT:UserName]", err.Error())
+					log.Println("[/]", err.Error())
 				}
 
-				repos = append(repos, row{r.Name, "", owner.Name, util.If(r.IsPrivate, "private", "public"), ""})
+				data.Repos = append(data.Repos, row{
+					d.Name, d.Description, owner.Name, util.If(d.IsPrivate, "private", "public"), "",
+				})
 			}
 		}
 
 		if err := rows.Err(); err != nil {
-			log.Println("[Index:SELECT:Err]", err.Error())
+			log.Println("[/]", err.Error())
 			HttpError(w, http.StatusInternalServerError)
-		} else if err := tmpl.ExecuteTemplate(w, "repo_index", struct {
-			Title string
-			Repos []row
-		}{"Repositories", repos}); err != nil {
-			log.Println("[Repo:Index]", err.Error())
+			return
+		}
+
+		if err := tmpl.ExecuteTemplate(w, "index", data); err != nil {
+			log.Println("[/]", err.Error())
 		}
 	}
 }
@@ -77,7 +81,7 @@ func HandleRepoCreate(w http.ResponseWriter, r *http.Request) {
 		name := r.FormValue("reponame")
 		private := r.FormValue("visibility") == "private"
 
-		if taken, err := RepoExists(db, name); err != nil {
+		if taken, err := RepoExists(name); err != nil {
 			log.Println("[RepoCreate:RepoExists]", err.Error())
 			HttpError(w, http.StatusInternalServerError)
 		} else if taken {
@@ -105,7 +109,7 @@ func HandleRepoCreate(w http.ResponseWriter, r *http.Request) {
 func HandleRepoLog(w http.ResponseWriter, r *http.Request) {
 	reponame := mux.Vars(r)["repo"]
 
-	repo, err := GetRepoByName(db, reponame)
+	repo, err := GetRepoByName(reponame)
 	if err != nil {
 		HttpError(w, http.StatusInternalServerError)
 		return
@@ -158,7 +162,7 @@ func HandleRepoTree(w http.ResponseWriter, r *http.Request) {
 func HandleRepoRefs(w http.ResponseWriter, r *http.Request) {
 	reponame := mux.Vars(r)["repo"]
 
-	repo, err := GetRepoByName(db, reponame)
+	repo, err := GetRepoByName(reponame)
 	if err != nil {
 		HttpError(w, http.StatusInternalServerError)
 		return
@@ -212,12 +216,28 @@ func HandleRepoRefs(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func GetRepoByName(db *sql.DB, name string) (*Repo, error) {
+func GetRepo(id uint64) (*Repo, error) {
+	r := &Repo{}
+
+	if err := db.QueryRow(
+		"SELECT id, owner_id, name, description, default_branch, is_private FROM repos WHERE id = ?", id,
+	).Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.DefaultBranch, &r.IsPrivate); err != nil {
+		if !errors.Is(err, sql.ErrNoRows) {
+			return nil, err
+		} else {
+			return nil, nil
+		}
+	} else {
+		return r, nil
+	}
+}
+
+func GetRepoByName(name string) (*Repo, error) {
 	r := &Repo{}
 
 	err := db.QueryRow(
-		"SELECT id, owner_id, name, name_lower, description, default_branch, is_private FROM repos WHERE name = ?", name,
-	).Scan(&r.Id, &r.OwnerId, &r.Name, &r.NameLower, &r.Description, &r.DefaultBranch, &r.IsPrivate)
+		"SELECT id, owner_id, name, description, default_branch, is_private FROM repos WHERE name = ?", name,
+	).Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.DefaultBranch, &r.IsPrivate)
 	if errors.Is(err, sql.ErrNoRows) {
 		return nil, nil
 	} else if err != nil {
@@ -227,7 +247,7 @@ func GetRepoByName(db *sql.DB, name string) (*Repo, error) {
 	return r, nil
 }
 
-func RepoExists(db *sql.DB, name string) (bool, error) {
+func RepoExists(name string) (bool, error) {
 	if err := db.QueryRow(
 		"SELECT name FROM repos WHERE name_lower = ?", strings.ToLower(name),
 	).Scan(&name); err != nil {