Author | Jakob Wakeling <[email protected]> |
Date | 2023-07-20 11:13:39 |
Commit | d631c5e22bf3af99f9be0e9cd8d8ea9ca0c14505 |
Parent | 6727af8703164d18b368d911ac94e4b7a7df9d7a |
Implement admin users, repos, and user edit pages
Also make use of XDG directories for config and data.
Diffstat
M | README.md | | | 15 | +++++++++++++++ |
M | go.mod | | | 1 | + |
M | go.sum | | | 8 | ++++++-- |
M | main.go | | | 10 | +++++----- |
A | res/admin/repos.html | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
A | res/admin/user_edit.html | | | 19 | +++++++++++++++++++ |
A | res/admin/users.html | | | 36 | ++++++++++++++++++++++++++++++++++++ |
D | res/admin_user_index.html | | | 32 | -------------------------------- |
R | res/base/head.html.tmpl -> res/base/head.html | | | 0 | |
R | res/base/repo_header.html.tmpl -> res/repo/header.html | | | 4 | ++-- |
R | res/error.html.tmpl -> res/error.html | | | 0 | |
M | res/res.go | | | 26 | ++++++++++++++++---------- |
A | res/user/login.html | | | 14 | ++++++++++++++ |
R | res/user_create.html -> res/admin/user_create.html | | | 12 | +++--------- |
D | res/user_login.html | | | 19 | ------------------- |
M | src/admin.go | | | 242 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- |
M | src/auth.go | | | 5 | +++-- |
M | src/git.go | | | 1 | - |
M | src/goit.go | | | 32 | ++++++++++++++++++++++---------- |
M | src/http.go | | | 9 | +++++++-- |
M | src/repo.go | | | 13 | +++++++------ |
M | src/user.go | | | 26 | +++++++++----------------- |
R | src/util.go -> src/util/util.go | | | 10 | ++-------- |
23 files changed, 389 insertions, 183 deletions
diff --git a/README.md b/README.md index f334d0d..6f5616e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,21 @@ A simple and lightweight Git web server. +## Features + +- Git Smart HTTP protocol (v2 only) +- Git SSH protocol (planned) +- Repository log, tree, refs, and commit viewers +- File viewer with syntax highlighting and markdown support (planned) +- File raw, blame, and history views (planned) +- Public and private repositories +- Read and write permissions for non owners (planned) +- Repository mirroring (pull and/or push) (planned) + +## Usage + +To build **Goit**, from the project root, run `go build`. + ## Meta Copyright (C) 2023, Jakob Wakeling diff --git a/go.mod b/go.mod index 7974e86..e1748a3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Jamozed/Goit go 1.20 require ( + github.com/adrg/xdg v0.4.0 github.com/go-git/go-git/v5 v5.7.0 github.com/gorilla/mux v1.8.0 github.com/mattn/go-sqlite3 v1.14.17 diff --git a/go.sum b/go.sum index 2b14a0d..6036ded 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2 github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -61,8 +63,9 @@ github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIn github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -92,6 +95,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -124,7 +128,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 28a77e2..36a08f7 100644 --- a/main.go +++ b/main.go @@ -16,8 +16,8 @@ import ( ) func main() { - if err := goit.InitGoit("./goit.json"); err != nil { - log.Fatalln("[InitGoit]", err.Error()) + if err := goit.Goit(goit.GetConfPath()); err != nil { + log.Fatalln(err.Error()) } h := mux.NewRouter() @@ -30,10 +30,10 @@ func main() { 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/user").Methods("GET").HandlerFunc(goit.HandleAdminUserIndex) - // h.Path("/admin/repos").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() + h.Path("/admin/user/edit").Methods("GET", "POST").HandlerFunc(goit.HandleAdminUserEdit) + h.Path("/admin/repos").Methods("GET").HandlerFunc(goit.HandleAdminRepos) h.Path("/{repo:.+(?:\\.git)$}").Methods(http.MethodGet).HandlerFunc(redirectDotGit) h.Path("/{repo}").Methods(http.MethodGet).HandlerFunc(goit.HandleRepoLog) diff --git a/res/admin/repos.html b/res/admin/repos.html new file mode 100644 index 0000000..db8b797 --- /dev/null +++ b/res/admin/repos.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<head>{{template "base/head" .}}</head> +<body> + <header> + <table> + <tr> + <td><img src = "/static/favicon.png" style="max-height: 24px"></td> + <td><h1>{{.Title}}</h1></td> + </tr> + </table> + </header> + <main> + <table> + <thead> + <tr> + <td><b>ID</b></td> + <td><b>Owner</b></td> + <td><b>Name</b></td> + <td><b>Visibility</b></td> + <td><b>Size</b></td> + <td></td> + </tr> + </thead> + <tbody> + {{range .Repos}} + <tr> + <td>{{.Id}}</td> + <td><a href="/?user={{.Owner}}">{{.Owner}}</a></td> + <td><a href="/{{.Name}}">{{.Name}}</a></td> + <td>{{.Visibility}}</td> + <td>{{.Size}}</td> + <td><a href="/admin/repo/edit?repo={{.Id}}">edit</a></td> + </tr> + {{end}} + </tbody> + </table> + </main> +</body> diff --git a/res/user_create.html b/res/admin/user_create.html similarity index 60% rename from res/user_create.html rename to res/admin/user_create.html index c4cfbd7..c19b879 100644 --- a/res/user_create.html +++ b/res/admin/user_create.html @@ -1,13 +1,7 @@ <!DOCTYPE html> -<head> - <meta charset="UTF-8"> - <title>Create User</title> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <link rel="stylesheet" type="text/css" href="/static/style.css"> - <link rel="icon" type="image/png" href="/static/favicon.png"> -</head> +<head>{{template "base/head" .}}</head> <body> - <h1>Create User</h1> + <h1>{{.Title}}</h1> <form action="/admin/user/create" method="post"> <label for="username">Username:</label> <input type="text" name="username"><br> @@ -19,5 +13,5 @@ <input type="checkbox" name="admin" value="true"><br> <input type="submit" value="Create"> </form> - <p>{{.Msg}}</p> + <span>{{.Message}}</span> </body> diff --git a/res/admin/user_edit.html b/res/admin/user_edit.html new file mode 100644 index 0000000..877a7b7 --- /dev/null +++ b/res/admin/user_edit.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<head>{{template "base/head" .}}</head> +<body> + <h1>{{.Title}}</h1> + <form action="/admin/user/edit?user={{.Id}}" method="post"> + <label for="id">ID:</label> + <input type="text" name="id" value="{{.Id}}" disabled><br> + <label for="username">Username:</label> + <input type="text" name="username" value="{{.Name}}"><br> + <label for="fullname">Full Name:</label> + <input type="text" name="fullname" value="{{.FullName}}"><br> + <label for="password">Password:</label> + <input type="password" name="password" placeholder="Unchanged"><br> + <label for="admin">Admin:</label> + <input type="checkbox" name="admin" value="true" {{if .IsAdmin}}checked{{end}}><br> + <input type="submit" value="Update"> + </form> + <span>{{.Message}}</span> +</body> diff --git a/res/admin/users.html b/res/admin/users.html new file mode 100644 index 0000000..b1d88bf --- /dev/null +++ b/res/admin/users.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<head>{{template "base/head" .}}</head> +<body> + <header> + <table> + <tr> + <td><img src = "/static/favicon.png" style="max-height: 24px"></td> + <td><h1>{{.Title}}</h1></td> + </tr> + </table> + </header> + <main> + <table> + <thead> + <tr> + <td><b>ID</b></td> + <td><b>Name</b></td> + <td><b>Full Name</b></td> + <td><b>Admin</b></td> + <td></td> + </tr> + </thead> + <tbody> + {{range .Users}} + <tr> + <td>{{.Id}}</td> + <td><a href="/?user={{.Name}}">{{.Name}}</a></td> + <td>{{.FullName}}</td> + <td>{{.IsAdmin}}</td> + <td><a href="/admin/user/edit?user={{.Id}}">edit</a></td> + </tr> + {{end}} + </tbody> + </table> + </main> +</body> diff --git a/res/admin_user_index.html b/res/admin_user_index.html deleted file mode 100644 index 08bc819..0000000 --- a/res/admin_user_index.html +++ /dev/null @@ -1,32 +0,0 @@ -<!DOCTYPE html> -<head> - <meta charset="UTF-8"> - <title>Users</title> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <link rel="stylesheet" type="text/css" href="/static/style.css"> - <link rel="icon" type="image/png" href="/static/favicon.png"> -</head> -<body> - <table> - <thead> - <tr> - <td><b>ID</b></td> - <td><b>Username</b></td> - <td><b>Full Name</b></td> - <td><b>Is Admin</b></td> - <td></td> - </tr> - </thead> - <tbody> - {{range .Users}} - <tr> - <td>{{.Id}}</td> - <td>{{.Name}}</td> - <td>{{.FullName}}</td> - <td>{{.IsAdmin}}</td> - <td><a>Edit</a></td> - </tr> - {{end}} - </tbody> - </table> -</body> diff --git a/res/base/head.html.tmpl b/res/base/head.html similarity index 100% rename from res/base/head.html.tmpl rename to res/base/head.html diff --git a/res/error.html.tmpl b/res/error.html similarity index 100% rename from res/error.html.tmpl rename to res/error.html diff --git a/res/base/repo_header.html.tmpl b/res/repo/header.html similarity index 92% rename from res/base/repo_header.html.tmpl rename to res/repo/header.html index b25b18e..3a97944 100644 --- a/res/base/repo_header.html.tmpl +++ b/res/repo/header.html @@ -18,10 +18,10 @@ <a href="/{{.Name}}/log">Log</a> | <a href="/{{.Name}}/tree">Tree</a> | <a href="/{{.Name}}/refs">Refs</a> - {{if .HasReadme}} + {{if .Readme}} | <a href="">README</a> {{end}} - {{if .HasLicence}} + {{if .Licence}} | <a href="">LICENCE</a> {{end}} </td> diff --git a/res/res.go b/res/res.go index 0f1cd3a..dd66d0b 100644 --- a/res/res.go +++ b/res/res.go @@ -2,19 +2,31 @@ package res import _ "embed" -//go:embed error.html.tmpl +//go:embed error.html var Error string -//go:embed base/head.html.tmpl +//go:embed base/head.html var BaseHead string -//go:embed base/repo_header.html.tmpl +//go:embed admin/users.html +var AdminUsers string + +//go:embed admin/user_create.html +var AdminUserCreate string + +//go:embed admin/user_edit.html +var AdminUserEdit string + +//go:embed admin/repos.html +var AdminRepos string + +//go:embed repo/header.html var RepoHeader string //go:embed repo_index.html var RepoIndex string -//go:embed user_login.html +//go:embed user/login.html var UserLogin string //go:embed repo_create.html @@ -29,11 +41,5 @@ var RepoTree string //go:embed repo_refs.html var RepoRefs string -//go:embed user_create.html -var UserCreate string - -//go:embed admin_user_index.html -var AdminUserIndex string - //go:embed style.css var Style string diff --git a/res/user_login.html b/res/user/login.html similarity index 52% rename from res/user_login.html rename to res/user/login.html index 0da5c64..24c080d 100644 --- a/res/user_login.html +++ b/res/user/login.html @@ -1,11 +1,6 @@ <!DOCTYPE html> -<head> - <meta charset="UTF-8"> - <title>Login</title> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <link rel="stylesheet" type="text/css" href="/static/style.css"> - <link rel="icon" type="image/png" href="/static/favicon.png"> -</head> +<head>{{template "base/head" .}}</head> +<body> <body> <h1>Login</h1> <form action="/user/login" method="post"> @@ -15,5 +10,5 @@ <input type="password" name="password"><br> <input type="submit" value="Login"> </form> - <p>{{.Msg}}</p> + <p>{{.Message}}</p> </body> diff --git a/src/admin.go b/src/admin.go index 7d134cf..45006c1 100644 --- a/src/admin.go +++ b/src/admin.go @@ -6,109 +6,235 @@ package goit import ( "fmt" - "html/template" "log" "net/http" + "strconv" "strings" - "github.com/Jamozed/Goit/res" + "github.com/Jamozed/Goit/src/util" ) -var ( - adminUserIndex *template.Template -) - -func init() { - adminUserIndex = template.Must(template.New("admin_user_index").Parse(res.AdminUserIndex)) -} - -func HandleAdminUserIndex(w http.ResponseWriter, r *http.Request) { - if ok, uid := AuthHttp(r); !ok { - HttpError(w, http.StatusNotFound) - return - } else if user, err := GetUser(uid); err != nil { - log.Println("[Admin:User:Create:Auth]", err.Error()) - HttpError(w, http.StatusNotFound) - return - } else if !user.IsAdmin { +func HandleAdminUsers(w http.ResponseWriter, r *http.Request) { + if !authHttpAdmin(r) { HttpError(w, http.StatusNotFound) return } - if rows, err := db.Query("SELECT id, name, name_full, is_admin FROM users"); err != nil { - log.Println("[Admin:User:Index:SELECT]", err.Error()) + rows, err := db.Query("SELECT id, name, name_full, is_admin FROM users") + if err != nil { + log.Println("[/admin/users]", err.Error()) HttpError(w, http.StatusInternalServerError) - } else { - defer rows.Close() + return + } - type row struct{ Id, Name, FullName, IsAdmin string } - users := []row{} + defer rows.Close() - for rows.Next() { - u := User{} + type row struct{ Id, Name, FullName, IsAdmin string } + data := struct { + Title string + Users []row + }{Title: "Users"} - if err := rows.Scan(&u.Id, &u.Name, &u.NameFull, &u.IsAdmin); err != nil { - log.Println("[Admin:User:Index:SELECT:Scan]", err.Error()) - } else { - users = append(users, row{fmt.Sprint(u.Id), u.Name, u.NameFull, If(u.IsAdmin, "true", "false")}) - } - } - - if err := rows.Err(); err != nil { - log.Println("[Admin:User:Index:SELECT:Err]", err.Error()) + for rows.Next() { + d := User{} + if err := rows.Scan(&d.Id, &d.Name, &d.FullName, &d.IsAdmin); err != nil { + log.Println("[/admin/users]", err.Error()) HttpError(w, http.StatusInternalServerError) - } else { - adminUserIndex.Execute(w, struct{ Users []row }{users}) + return } + + data.Users = append(data.Users, row{ + fmt.Sprint(d.Id), d.Name, d.FullName, util.If(d.IsAdmin, "true", "false"), + }) + } + + if err := rows.Err(); err != nil { + log.Println("[/admin/users]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return + } + + if err := tmpl.ExecuteTemplate(w, "admin/users", data); err != nil { + log.Println("[/admin/users]", err.Error()) } } func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) { - if ok, uid := AuthHttp(r); !ok { - HttpError(w, http.StatusNotFound) - return - } else if user, err := GetUser(uid); err != nil { - log.Println("[Admin:User:Create:Auth]", err.Error()) - HttpError(w, http.StatusNotFound) - return - } else if !user.IsAdmin { + if !authHttpAdmin(r) { HttpError(w, http.StatusNotFound) return } - data := struct{ Msg string }{""} + data := struct{ Title, Message string }{"Create User", ""} if r.Method == http.MethodPost { username := strings.ToLower(r.FormValue("username")) fullname := r.FormValue("fullname") password := r.FormValue("password") - admin := r.FormValue("admin") == "true" + isAdmin := r.FormValue("admin") == "true" if username == "" { - data.Msg = "Username cannot be empty" - } else if SliceContains(reserved, username) { - data.Msg = "Username \"" + username + "\" is reserved" + data.Message = "Username cannot be empty" + } else if util.SliceContains(reserved, username) { + data.Message = "Username \"" + username + "\" is reserved" } else if exists, err := UserExists(username); err != nil { - log.Println("[Admin:User:Create:Exists]", err.Error()) + log.Println("[/admin/user/create]", err.Error()) HttpError(w, http.StatusInternalServerError) return } else if exists { - data.Msg = "Username \"" + username + "\" is taken" + data.Message = "Username \"" + username + "\" is taken" } else if salt, err := Salt(); err != nil { - log.Println("[Admin:User:Create:Salt]", err.Error()) + log.Println("[/admin/user/create]", err.Error()) HttpError(w, http.StatusInternalServerError) return } else if _, err := db.Exec( "INSERT INTO users (name, name_full, pass, pass_algo, salt, is_admin) VALUES (?, ?, ?, ?, ?, ?)", - username, fullname, Hash(password, salt), "argon2", salt, admin, + username, fullname, Hash(password, salt), "argon2", salt, isAdmin, ); err != nil { - log.Println("[Admin:User:Create:INSERT]", err.Error()) + log.Println("[/admin/user/create]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return + } else { + data.Message = "User \"" + username + "\" created successfully" + } + } + + 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) { + HttpError(w, http.StatusNotFound) + return + } + + id, err := strconv.ParseUint(r.URL.Query().Get("user"), 10, 64) + if err != nil { + HttpError(w, http.StatusNotFound) + return + } + + user, err := GetUser(id) + if err != nil { + log.Println("[/admin/user/edit]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return + } else if user == nil { + HttpError(w, http.StatusNotFound) + return + } + + 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} + + if r.Method == http.MethodPost { + data.Name = strings.ToLower(r.FormValue("username")) + data.FullName = r.FormValue("fullname") + password := r.FormValue("password") + data.IsAdmin = r.FormValue("admin") == "true" + + if data.Name == "" { + data.Message = "Username cannot be empty" + } else if util.SliceContains(reserved, data.Name) { + data.Message = "Username \"" + data.Name + "\" is reserved" + } else if exists, err := UserExists(data.Name); err != nil { + log.Println("[/admin/user/edit]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return + } else if exists && data.Name != user.Name { + data.Message = "Username \"" + data.Name + "\" is taken" + } else if salt, err := Salt(); err != nil { + log.Println("[/admin/user/edit]", err.Error()) HttpError(w, http.StatusInternalServerError) return } else { - data.Msg = "User \"" + username + "\" created successfully" + if password == "" { + _, err = db.Exec( + "UPDATE users SET name = ?, name_full = ?, is_admin = ? WHERE id = ?", + data.Name, data.FullName, data.IsAdmin, user.Id, + ) + } else { + _, err = db.Exec( + "UPDATE users SET name = ?, name_full = ?, pass = ?, salt = ?, is_admin = ? WHERE id = ?", + data.Name, data.FullName, Hash(password, salt), salt, data.IsAdmin, user.Id, + ) + } + + if err != nil { + log.Println("[/admin/user/edit]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return + } else { + data.Message = "User \"" + user.Name + "\" updated successfully" + } + } + } + + 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) { + HttpError(w, http.StatusNotFound) + return + } + + rows, err := db.Query("SELECT id, owner_id, name, is_private FROM repos") + if err != nil { + log.Println("[/admin/repos]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return + } + + defer rows.Close() + + type row struct{ Id, Owner, Name, Visibility, Size string } + data := struct { + Title string + Repos []row + }{Title: "Repos"} + + for rows.Next() { + d := Repo{} + if err := rows.Scan(&d.Id, &d.OwnerId, &d.Name, &d.IsPrivate); err != nil { + log.Println("[/admin/repos]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return + } + + user, err := GetUser(d.OwnerId) + if err != nil { + log.Println("[/admin/repos]", err.Error()) + } + + data.Repos = append(data.Repos, row{ + fmt.Sprint(d.Id), user.Name, d.Name, util.If(d.IsPrivate, "private", "public"), "", + }) + } + + if err := rows.Err(); err != nil { + log.Println("[/admin/repos]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return + } + + if err := tmpl.ExecuteTemplate(w, "admin/repos", data); err != nil { + log.Println("[/admin/repos]", err.Error()) + } +} + +func authHttpAdmin(r *http.Request) bool { + if ok, uid := AuthHttp(r); ok { + if user, err := GetUser(uid); err == nil && user.IsAdmin { + return true } } - userCreate.Execute(w, data) + return false } diff --git a/src/auth.go b/src/auth.go index 15ace19..d2fb2c0 100644 --- a/src/auth.go +++ b/src/auth.go @@ -13,6 +13,7 @@ import ( "net/http" "time" + "github.com/Jamozed/Goit/src/util" "golang.org/x/crypto/argon2" ) @@ -66,7 +67,7 @@ func Auth(s string) (bool, uint64) { } func AuthHttp(r *http.Request) (bool, uint64) { - if c := Cookie(r, "session"); c != nil { + if c := util.Cookie(r, "session"); c != nil { return Auth(c.Value) } @@ -74,7 +75,7 @@ func AuthHttp(r *http.Request) (bool, uint64) { } func SessionCookie(r *http.Request) string { - if c := Cookie(r, "session"); c != nil { + if c := util.Cookie(r, "session"); c != nil { return c.Value } diff --git a/src/git.go b/src/git.go index df2d8c8..bef8f2d 100644 --- a/src/git.go +++ b/src/git.go @@ -180,7 +180,6 @@ func gitHttpRpc(w http.ResponseWriter, r *http.Request, service string, repo *Re } func pktLine(str string) []byte { - PanicIf(len(str) > 65516, "pktLine: Payload exceeds maximum length") s := strconv.FormatUint(uint64(len(str)+4), 16) s = strings.Repeat("0", 4-len(s)%4) + s return []byte(s + str) diff --git a/src/goit.go b/src/goit.go index d14a58e..b9b21e3 100644 --- a/src/goit.go +++ b/src/goit.go @@ -13,6 +13,7 @@ import ( "os" "path" + "github.com/adrg/xdg" _ "github.com/mattn/go-sqlite3" ) @@ -24,7 +25,7 @@ type Config struct { } var Conf = Config{ - DataPath: ".", + DataPath: path.Join(xdg.DataHome, "goit"), HttpAddr: "", HttpPort: "8080", GitPath: "git", @@ -33,8 +34,7 @@ var Conf = Config{ var db *sql.DB var Favicon []byte -/* Initialise Goit. */ -func InitGoit(conf string) (err error) { +func Goit(conf string) (err error) { if dat, err := os.ReadFile(conf); err != nil { if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("[Config] %w", err) @@ -45,10 +45,13 @@ func InitGoit(conf string) (err error) { } } + log.Println("[Config] using data path:", Conf.DataPath) + if err := os.MkdirAll(Conf.DataPath, 0o777); err != nil { + return fmt.Errorf("[Config] %w", err) + } + if dat, err := os.ReadFile(path.Join(Conf.DataPath, "favicon.png")); err != nil { - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("[Config] %w", err) - } + log.Println("[Favicon]", err.Error()) } else { Favicon = dat } @@ -82,22 +85,22 @@ func InitGoit(conf string) (err error) { is_private BOOLEAN NOT NULL )`, ); err != nil { - return fmt.Errorf("[CREATE:repos] %w", err) + return fmt.Errorf("[CREATE repos] %w", err) } /* Create an admin user if one does not exist */ if exists, err := UserExists("admin"); err != nil { - log.Println("[admin:Exists]", err.Error()) + log.Println("[admin Exists]", err.Error()) err = nil /* ignored */ } else if !exists { if salt, err := Salt(); err != nil { - log.Println("[admin:Salt]", err.Error()) + log.Println("[admin Salt]", err.Error()) err = nil /* ignored */ } else if _, err = db.Exec( "INSERT INTO users (id, name, name_full, pass, pass_algo, salt, is_admin) VALUES (?, ?, ?, ?, ?, ?, ?)", 0, "admin", "Administrator", Hash("admin", salt), "argon2", salt, true, ); err != nil { - log.Println("[admin:INSERT]", err.Error()) + log.Println("[admin INSERT]", err.Error()) err = nil /* ignored */ } } @@ -105,6 +108,15 @@ func InitGoit(conf string) (err error) { return nil } +func GetConfPath() string { + if p, err := xdg.SearchConfigFile(path.Join("goit", "goit.json")); err != nil { + log.Println("[Config]", err.Error()) + return "" + } else { + return p + } +} + func GetRepoPath(name string) string { return path.Join(Conf.DataPath, "repos", name+".git") } diff --git a/src/http.go b/src/http.go index b3c293c..60f5be0 100644 --- a/src/http.go +++ b/src/http.go @@ -15,10 +15,15 @@ import ( var tmpl = template.Must(template.New("error").Parse(res.Error)) func init() { - tmpl.Option("missingkey=zero") - template.Must(tmpl.New("base/head").Parse(res.BaseHead)) + + 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/repos").Parse(res.AdminRepos)) + 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)) diff --git a/src/repo.go b/src/repo.go index 4220c8b..8ed70f0 100644 --- a/src/repo.go +++ b/src/repo.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/Jamozed/Goit/src/util" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" @@ -51,7 +52,7 @@ func HandleIndex(w http.ResponseWriter, r *http.Request) { log.Println("[Index:SELECT:UserName]", err.Error()) } - repos = append(repos, row{r.Name, "", owner.Name, If(r.IsPrivate, "private", "public"), ""}) + repos = append(repos, row{r.Name, "", owner.Name, util.If(r.IsPrivate, "private", "public"), ""}) } } @@ -79,7 +80,7 @@ func HandleRepoCreate(w http.ResponseWriter, r *http.Request) { HttpError(w, http.StatusInternalServerError) } else if taken { tmpl.ExecuteTemplate(w, "repo_create", struct{ Msg string }{"Reponame is taken"}) - } else if SliceContains[string](reserved, name) { + } else if util.SliceContains[string](reserved, name) { tmpl.ExecuteTemplate(w, "repo_create", struct{ Msg string }{"Reponame is reserved"}) } else { if _, err := db.Exec( @@ -139,10 +140,10 @@ func HandleRepoLog(w http.ResponseWriter, r *http.Request) { if err := tmpl.ExecuteTemplate(w, "repo_log", struct { Title, Name, Description, Url string - HasReadme, HasLicence bool + Readme, Licence string Commits []row }{ - "Log", reponame, repo.Description, r.URL.Host + "/" + repo.Name + ".git", false, false, commits, + "Log", reponame, repo.Description, r.URL.Host + "/" + repo.Name + ".git", "", "", commits, }); err != nil { log.Println("[Repo:Log]", err.Error()) } @@ -199,11 +200,11 @@ func HandleRepoRefs(w http.ResponseWriter, r *http.Request) { if err := tmpl.ExecuteTemplate(w, "repo_refs", struct { Title, Name, Description, Url string - HasReadme, HasLicence bool + Readme, Licence string Branches []bra Tags []tag }{ - "Refs", reponame, repo.Description, r.URL.Host + "/" + repo.Name + ".git", false, false, bras, tags, + "Refs", reponame, repo.Description, r.URL.Host + "/" + repo.Name + ".git", "", "", bras, tags, }); err != nil { log.Println("[Repo:Refs]", err.Error()) } diff --git a/src/user.go b/src/user.go index f60fe7f..77c11a9 100644 --- a/src/user.go +++ b/src/user.go @@ -9,31 +9,23 @@ import ( "database/sql" "errors" "fmt" - "html/template" "log" "net/http" "strings" "time" - - "github.com/Jamozed/Goit/res" ) type User struct { Id uint64 Name string - NameFull string + FullName string Pass []byte PassAlgo string Salt []byte IsAdmin bool } -var ( - reserved []string = []string{"admin", "repo", "static", "user"} - - userLogin *template.Template = template.Must(template.New("user_login").Parse(res.UserLogin)) - userCreate *template.Template = template.Must(template.New("user_create").Parse(res.UserCreate)) -) +var reserved []string = []string{"admin", "repo", "static", "user"} func HandleUserLogin(w http.ResponseWriter, r *http.Request) { if ok, _ := AuthHttp(r); ok { @@ -41,7 +33,7 @@ func HandleUserLogin(w http.ResponseWriter, r *http.Request) { return } - data := struct{ Msg string }{""} + data := struct{ Title, Message string }{"Login", ""} if r.Method == http.MethodPost { u := User{} @@ -49,13 +41,13 @@ func HandleUserLogin(w http.ResponseWriter, r *http.Request) { password := r.FormValue("password") if username == "" { - data.Msg = "Username cannot be empty" + data.Message = "Username cannot be empty" } else if exists, err := UserExists(username); err != nil { log.Println("[User:Login:Exists]", err.Error()) HttpError(w, http.StatusInternalServerError) return } else if !exists { - data.Msg = "Invalid credentials" + data.Message = "Invalid credentials" } else if err := db.QueryRow( "SELECT id, name, pass, pass_algo, salt FROM users WHERE name = ?", username, ).Scan(&u.Id, &u.Name, &u.Pass, &u.PassAlgo, &u.Salt); err != nil { @@ -63,7 +55,7 @@ func HandleUserLogin(w http.ResponseWriter, r *http.Request) { HttpError(w, http.StatusInternalServerError) return } else if !bytes.Equal(Hash(password, u.Salt), u.Pass) { - data.Msg = "Invalid credentials" + data.Message = "Invalid credentials" } else { expiry := time.Now().Add(15 * time.Minute) if s, err := NewSession(u.Id, expiry); err != nil { @@ -78,7 +70,7 @@ func HandleUserLogin(w http.ResponseWriter, r *http.Request) { } } - userLogin.Execute(w, data) + tmpl.ExecuteTemplate(w, "user_login", data) } func HandleUserLogout(w http.ResponseWriter, r *http.Request) { @@ -92,7 +84,7 @@ func GetUser(id uint64) (*User, error) { if err := db.QueryRow( "SELECT id, name, name_full, is_admin FROM users WHERE id = ?", id, - ).Scan(&u.Id, &u.Name, &u.NameFull, &u.IsAdmin); err != nil { + ).Scan(&u.Id, &u.Name, &u.FullName, &u.IsAdmin); err != nil { if !errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("[SELECT:user] %w", err) } else { @@ -108,7 +100,7 @@ func GetUserByName(name string) (*User, error) { err := db.QueryRow( "SELECT id, name, name_full, pass, pass_algo, salt, is_admin FROM users WHERE name = ?", strings.ToLower(name), - ).Scan(&u.Id, &u.Name, &u.NameFull, &u.Pass, &u.PassAlgo, &u.Salt, &u.IsAdmin) + ).Scan(&u.Id, &u.Name, &u.FullName, &u.Pass, &u.PassAlgo, &u.Salt, &u.IsAdmin) if errors.Is(err, sql.ErrNoRows) { return nil, nil } else if err != nil { diff --git a/src/util.go b/src/util/util.go similarity index 85% rename from src/util.go rename to src/util/util.go index 3cd9efc..065fd85 100644 --- a/src/util.go +++ b/src/util/util.go @@ -1,8 +1,8 @@ -// util.go +// util/util.go // Copyright (C) 2023, Jakob Wakeling // All rights reserved. -package goit +package util import ( "net/http" @@ -26,12 +26,6 @@ func SliceContains[T comparable](s []T, e T) bool { return false } -func PanicIf(cond bool, v any) { - if cond { - panic(v) - } -} - /* Return the named cookie or nil if not found. */ func Cookie(r *http.Request, name string) *http.Cookie { if c, err := r.Cookie(name); err != nil {