Author | Jakob Wakeling <[email protected]> |
Date | 2023-07-21 02:52:36 |
Commit | a0ba6ae56e9aa32404aeaa74ab9792b3525eb81b |
Parent | 4a160dd13c06c4a572a55de208200f686a771eaf |
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 {