Goit

Simple and lightweight Git web server
Mirror of https://github.com/Jamozed/Goit
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-11-22 10:25:37
Commit463cf58b6afaa428962644e198ac283f0d8ddd9c
Parent7dd44593815e8cb2019e0757ac1900a3da470755

Update user and repo admin pages

Diffstat

M res/admin/repo_edit.html | 52 ++++++++++++++++++++++++++++++++++++++++------------
M res/admin/user_edit.html | 44 +++++++++++++++++++++++++++++++++++---------
A src/admin/admin.go | 25 +++++++++++++++++++++++++
A src/admin/repos.go | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A src/admin/users.go | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
D src/goit/admin.go | 321 --------------------------------------------------------------------------------
M src/goit/repo.go | 26 ++++++++++++++++++++++++++
M src/goit/user.go | 43 +++++++++++++++++++++++++++++++++++++++++--
M src/main.go | 13 +++++++------
M src/repo/edit.go | 2 ++
M src/user/edit.go | 2 +-

11 files changed, 517 insertions, 351 deletions

diff --git a/res/admin/repo_edit.html b/res/admin/repo_edit.html
index 4cb6fdb..76ddf3e 100644
--- a/res/admin/repo_edit.html
+++ b/res/admin/repo_edit.html
@@ -1,24 +1,45 @@
 <!DOCTYPE html>
 <head lang="en-GB">{{template "base/head" .}}</head>
 <body>
+	<header>
+		<table>
+			<tr>
+				<td rowspan="2">
+					<a href="/"><img src="/static/favicon.png" style="max-height: 24px"></a>
+				</td>
+				<td><h1>{{.Title}}</h1></td>
+			</tr>
+			<tr><td>
+				<a href="/admin/users">Users</a>
+				| <a href="/admin/repos">Repositories</a>
+			</td></tr>
+		</table>
+	</header><hr>
 	<main>
-		<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">
+		<h1>{{.Title}}</h1><hr>
+		<form action="/admin/repo/edit?repo={{.Form.Id}}" method="post">
+			<table>
+				<tr><td><label for="id">ID</label></td></tr>
+				<tr><td><span>{{.Form.Id}}</span></td></tr>
+				<tr><td><label for="id">Owner</label></td></tr>
+				<tr><td><span>{{.Form.Owner}}</span></td></tr>
+				<tr><td><label for="reponame">Name</label></td></tr>
+				<tr><td><input type="text" name="reponame" value="{{.Form.Name}}" spellcheck="false"></td></tr>
+				<tr><td><label for="description">Description</label></td></tr>
+				<tr><td><textarea name="description" spellcheck="false">{{.Form.Description}}</textarea></td></tr>
+				<tr><td><label for="visibility">Visibility:</label></td></tr>
+				<tr><td>
+					<select name="visibility">
+						<option value="public">Public</option>
+						<option value="private" {{if .Form.IsPrivate}}selected{{end}}>Private</option>
+					</select>
+				</td></tr>
+				<tr><td>
+					<input type="submit" name="submit" value="Update">
+					<a href="/admin/repos" style="color: inherit;">Cancel</a>
+				</td></tr>
+				<tr><td><span style="color: #AA0000">{{.Message}}</span></td></tr>
+			</table>
 		</form>
-		<span>{{.Message}}</span>
 	</main>
 </body>
diff --git a/res/admin/user_edit.html b/res/admin/user_edit.html
index 43badf4..3424289 100644
--- a/res/admin/user_edit.html
+++ b/res/admin/user_edit.html
@@ -1,21 +1,40 @@
 <!DOCTYPE html>
 <head lang="en-GB">{{template "base/head" .}}</head>
 <body>
+	<header>
+		<table>
+			<tr>
+				<td rowspan="2">
+					<a href="/"><img src="/static/favicon.png" style="max-height: 24px"></a>
+				</td>
+				<td><h1>{{.Title}}</h1></td>
+			</tr>
+			<tr><td>
+				<a href="/admin/users">Users</a>
+				| <a href="/admin/repos">Repositories</a>
+			</td></tr>
+		</table>
+	</header><hr>
 	<main>
-		<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">
+		<h1>{{.Title}}</h1><hr>
+		<form action="/admin/user/edit?user={{.Form.Id}}" method="post">
+			<table>
+				<tr><td><label for="id">ID</label></td></tr>
+				<tr><td><input type="text" name="id" value="{{.Form.Id}}" spellcheck="false" disabled></td></tr>
+				<tr><td><label for="username">Username</label></td></tr>
+				<tr><td><input type="text" name="username" value="{{.Form.Name}}" spellcheck="false"></td></tr>
+				<tr><td><label for="fullname">Full Name</label></td></tr>
+				<tr><td><input type="text" name="fullname" value="{{.Form.FullName}}" spellcheck="false"></td></tr>
+				<tr><td><label for="password">Password</label></td></tr>
+				<tr><td><input type="password" name="password" placeholder="unchanged"></td></tr>
+				<tr><td><label for="admin">Admin</label></td></tr>
+				<tr><td><input type="checkbox" name="admin" value="true" {{if .Form.IsAdmin}}checked{{end}}></td></tr>
+				<tr><td>
+					<input type="submit" name="submit" value="Update">
+					<a href="/admin/users" style="color: inherit;">Cancel</a>
+					<span style="color: #AA0000">{{.Message}}</span>
+				</td></tr>
+			</table>
 		</form>
-		<span>{{.Message}}</span>
 	</main>
 </body>
diff --git a/src/admin/admin.go b/src/admin/admin.go
new file mode 100644
index 0000000..bff2609
--- /dev/null
+++ b/src/admin/admin.go
@@ -0,0 +1,25 @@
+package admin
+
+import (
+	"log"
+	"net/http"
+
+	"github.com/Jamozed/Goit/src/goit"
+)
+
+func HandleIndex(w http.ResponseWriter, r *http.Request) {
+	auth, user, err := goit.Auth(w, r, true)
+	if err != nil {
+		log.Println("[admin]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+	}
+
+	if !auth || !user.IsAdmin {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "admin/index", struct{ Title string }{"Admin"}); err != nil {
+		log.Println("[/admin/index]", err.Error())
+	}
+}
diff --git a/src/admin/repos.go b/src/admin/repos.go
new file mode 100644
index 0000000..811d83f
--- /dev/null
+++ b/src/admin/repos.go
@@ -0,0 +1,151 @@
+package admin
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"slices"
+	"strconv"
+
+	"github.com/Jamozed/Goit/src/goit"
+	"github.com/Jamozed/Goit/src/util"
+	"github.com/dustin/go-humanize"
+)
+
+func HandleRepos(w http.ResponseWriter, r *http.Request) {
+	auth, user, err := goit.Auth(w, r, true)
+	if err != nil {
+		log.Println("[admin/users]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	if !auth || !user.IsAdmin {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	type row struct{ Id, Owner, Name, Visibility, Size string }
+	data := struct {
+		Title string
+		Repos []row
+	}{Title: "Admin - Repositories"}
+
+	repos, err := goit.GetRepos()
+	if err != nil {
+		log.Println("[/admin/repos]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	for _, r := range repos {
+		u, err := goit.GetUser(r.OwnerId)
+		if err != nil {
+			log.Println("[/admin/repos]", err.Error())
+			u = &goit.User{}
+		}
+
+		size, err := util.DirSize(goit.RepoPath(r.Name, true))
+		if err != nil {
+			log.Println("[/admin/repos]", err.Error())
+		}
+
+		data.Repos = append(data.Repos, row{
+			fmt.Sprint(r.Id), u.Name, r.Name, util.If(r.IsPrivate, "private", "public"), humanize.IBytes(size),
+		})
+	}
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "admin/repos", data); err != nil {
+		log.Println("[/admin/repos]", err.Error())
+	}
+}
+
+func HandleRepoEdit(w http.ResponseWriter, r *http.Request) {
+	auth, user, err := goit.Auth(w, r, true)
+	if err != nil {
+		log.Println("[admin/users]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	if !auth || !user.IsAdmin {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	id, err := strconv.ParseInt(r.URL.Query().Get("repo"), 10, 64)
+	if err != nil {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	repo, err := goit.GetRepo(id)
+	if err != nil {
+		log.Println("[/admin/repo/edit]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	} else if repo == nil {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	owner, err := goit.GetUser(repo.OwnerId)
+	if err != nil {
+		log.Println("[/admin/repo/edit]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	} else if owner == nil {
+		log.Println("[/admin/repo/edit]", repo.Id, "is owned by a nonexistent user")
+		/* TODO have admin adopt the orphaned repository */
+		owner = &goit.User{}
+	}
+
+	data := struct {
+		Title, Message string
+
+		Form struct {
+			Id, Owner, Name, Description string
+			IsPrivate                    bool
+		}
+	}{
+		Title: "Admin - Edit Repository",
+	}
+
+	data.Form.Id = fmt.Sprint(repo.Id)
+	data.Form.Owner = owner.FullName + " (" + owner.Name + ")[" + fmt.Sprint(owner.Id) + "]"
+	data.Form.Name = repo.Name
+	data.Form.Description = repo.Description
+	data.Form.IsPrivate = repo.IsPrivate
+
+	if r.Method == http.MethodPost {
+		data.Form.Name = r.FormValue("reponame")
+		data.Form.Description = r.FormValue("description")
+		data.Form.IsPrivate = r.FormValue("visibility") == "private"
+
+		if data.Form.Name == "" {
+			data.Message = "Name cannot be empty"
+		} else if slices.Contains(goit.Reserved, data.Form.Name) {
+			data.Message = "Name \"" + data.Form.Name + "\" is reserved"
+		} else if exists, err := goit.RepoExists(data.Form.Name); err != nil {
+			log.Println("[/admin/repo/edit]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else if exists && data.Form.Name != repo.Name {
+			data.Message = "Name \"" + data.Form.Name + "\" is taken"
+		} else if len(data.Form.Description) > 256 {
+			data.Message = "Description cannot exceed 256 characters"
+		} else if err := goit.UpdateRepo(repo.Id, goit.Repo{
+			Name: data.Form.Name, Description: data.Form.Description, IsPrivate: data.Form.IsPrivate,
+		}); err != nil {
+			log.Println("[/admin/repo/edit]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else {
+			data.Message = "Repository \"" + repo.Name + "\" updated successfully"
+		}
+	}
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "admin/repo/edit", data); err != nil {
+		log.Println("[/admin/repo/edit]", err.Error())
+	}
+}
diff --git a/src/admin/users.go b/src/admin/users.go
new file mode 100644
index 0000000..eddad46
--- /dev/null
+++ b/src/admin/users.go
@@ -0,0 +1,189 @@
+package admin
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"slices"
+	"strconv"
+	"strings"
+
+	"github.com/Jamozed/Goit/src/goit"
+	"github.com/Jamozed/Goit/src/util"
+)
+
+func HandleUsers(w http.ResponseWriter, r *http.Request) {
+	auth, user, err := goit.Auth(w, r, true)
+	if err != nil {
+		log.Println("[admin/users]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	if !auth || !user.IsAdmin {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	type row struct{ Id, Name, FullName, IsAdmin string }
+	data := struct {
+		Title string
+		Users []row
+	}{Title: "Admin - Users"}
+
+	users, err := goit.GetUsers()
+	if err != nil {
+		log.Println("[admin/users]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	for _, u := range users {
+		data.Users = append(data.Users, row{
+			fmt.Sprint(u.Id), u.Name, u.FullName, util.If(u.IsAdmin, "true", "false"),
+		})
+	}
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "admin/users", data); err != nil {
+		log.Println("[/admin/users]", err.Error())
+	}
+}
+
+func HandleUserCreate(w http.ResponseWriter, r *http.Request) {
+	auth, user, err := goit.Auth(w, r, true)
+	if err != nil {
+		log.Println("[admin/users]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	if !auth || !user.IsAdmin {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	data := struct{ Title, Message string }{"Admin - Create User", ""}
+
+	if r.Method == http.MethodPost {
+		username := strings.ToLower(r.FormValue("username"))
+		fullname := r.FormValue("fullname")
+		password := r.FormValue("password")
+		isAdmin := r.FormValue("admin") == "true"
+
+		if username == "" {
+			data.Message = "Username cannot be empty"
+		} else if slices.Contains(goit.Reserved, username) {
+			data.Message = "Username \"" + username + "\" is reserved"
+		} else if exists, err := goit.UserExists(username); err != nil {
+			log.Println("[/admin/user/create]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else if exists {
+			data.Message = "Username \"" + username + "\" is taken"
+		} else if salt, err := goit.Salt(); err != nil {
+			log.Println("[/admin/user/create]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else if err := goit.CreateUser(goit.User{
+			Name: username, FullName: fullname, Pass: goit.Hash(password, salt), PassAlgo: "argon2", Salt: salt,
+			IsAdmin: isAdmin,
+		}); err != nil {
+			log.Println("[/admin/user/create]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else {
+			data.Message = "User \"" + username + "\" created successfully"
+		}
+	}
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "admin/user/create", data); err != nil {
+		log.Println("[/admin/user/create]", err.Error())
+	}
+}
+
+func HandleUserEdit(w http.ResponseWriter, r *http.Request) {
+	auth, user, err := goit.Auth(w, r, true)
+	if err != nil {
+		log.Println("[admin/users]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	if !auth || !user.IsAdmin {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	uid, err := strconv.ParseInt(r.URL.Query().Get("user"), 10, 64)
+	if err != nil {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	u, err := goit.GetUser(uid)
+	if err != nil {
+		log.Println("[/admin/user/edit]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	} else if u == nil {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	data := struct {
+		Title, Message string
+
+		Form struct {
+			Id, Name, FullName string
+			IsAdmin            bool
+		}
+	}{
+		Title: "Admin - Edit User",
+	}
+
+	data.Form.Id = fmt.Sprint(u.Id)
+	data.Form.Name = u.Name
+	data.Form.FullName = u.FullName
+	data.Form.IsAdmin = u.IsAdmin
+
+	if r.Method == http.MethodPost {
+		data.Form.Name = strings.ToLower(r.FormValue("username"))
+		data.Form.FullName = r.FormValue("fullname")
+		password := r.FormValue("password")
+		data.Form.IsAdmin = r.FormValue("admin") == "true"
+
+		if data.Form.Name == "" {
+			data.Message = "Username cannot be empty"
+		} else if slices.Contains(goit.Reserved, data.Form.Name) {
+			data.Message = "Username \"" + data.Form.Name + "\" is reserved"
+		} else if exists, err := goit.UserExists(data.Form.Name); err != nil {
+			log.Println("[/admin/user/edit]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else if exists && data.Form.Name != u.Name {
+			data.Message = "Username \"" + data.Form.Name + "\" is taken"
+		} else {
+			if err := goit.UpdateUser(u.Id, goit.User{
+				Name: data.Form.Name, FullName: data.Form.FullName, IsAdmin: data.Form.IsAdmin,
+			}); err != nil {
+				log.Println("[/admin/user/edit]", err.Error())
+				goit.HttpError(w, http.StatusInternalServerError)
+				return
+			}
+
+			if password != "" {
+				if err := goit.UpdatePassword(u.Id, password); err != nil {
+					log.Println("[/admin/user/edit]", err.Error())
+					goit.HttpError(w, http.StatusInternalServerError)
+					return
+				}
+			}
+
+			data.Message = "User \"" + u.Name + "\" updated successfully"
+		}
+	}
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "admin/user/edit", data); err != nil {
+		log.Println("[/admin/user/edit]", err.Error())
+	}
+}
diff --git a/src/goit/admin.go b/src/goit/admin.go
deleted file mode 100644
index 9804e38..0000000
--- a/src/goit/admin.go
+++ /dev/null
@@ -1,321 +0,0 @@
-// admin.go
-// Copyright (C) 2023, Jakob Wakeling
-// All rights reserved.
-
-package goit
-
-import (
-	"fmt"
-	"log"
-	"net/http"
-	"slices"
-	"strconv"
-	"strings"
-
-	"github.com/Jamozed/Goit/src/util"
-	"github.com/dustin/go-humanize"
-)
-
-func HandleAdminIndex(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
-		HttpError(w, http.StatusNotFound)
-		return
-	}
-
-	if err := Tmpl.ExecuteTemplate(w, "admin/index", struct{ Title string }{"Admin"}); err != nil {
-		log.Println("[/admin/index]", err.Error())
-	}
-}
-
-func HandleAdminUsers(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
-		HttpError(w, http.StatusNotFound)
-		return
-	}
-
-	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)
-		return
-	}
-
-	defer rows.Close()
-
-	type row struct{ Id, Name, FullName, IsAdmin string }
-	data := struct {
-		Title string
-		Users []row
-	}{Title: "Admin - Users"}
-
-	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)
-			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 _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
-		HttpError(w, http.StatusNotFound)
-		return
-	}
-
-	data := struct{ Title, Message string }{"Admin - Create User", ""}
-
-	if r.Method == http.MethodPost {
-		username := strings.ToLower(r.FormValue("username"))
-		fullname := r.FormValue("fullname")
-		password := r.FormValue("password")
-		isAdmin := r.FormValue("admin") == "true"
-
-		if username == "" {
-			data.Message = "Username cannot be empty"
-		} else if slices.Contains(Reserved, username) {
-			data.Message = "Username \"" + username + "\" is reserved"
-		} else if exists, err := UserExists(username); err != nil {
-			log.Println("[/admin/user/create]", err.Error())
-			HttpError(w, http.StatusInternalServerError)
-			return
-		} else if exists {
-			data.Message = "Username \"" + username + "\" is taken"
-		} else if salt, err := Salt(); err != nil {
-			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, isAdmin,
-		); err != nil {
-			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 _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
-		HttpError(w, http.StatusNotFound)
-		return
-	}
-
-	id, err := strconv.ParseInt(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: "Admin - 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 slices.Contains(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 {
-			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 _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
-		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: "Admin - Repositories"}
-
-	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())
-		}
-
-		size, err := util.DirSize(RepoPath(d.Name, true))
-		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"), humanize.IBytes(size),
-		})
-	}
-
-	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 HandleAdminRepoEdit(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
-		HttpError(w, http.StatusNotFound)
-		return
-	}
-
-	id, err := strconv.ParseInt(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: "Admin - 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 slices.Contains(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"
-		}
-	}
-
-	if err := Tmpl.ExecuteTemplate(w, "admin/repo/edit", data); err != nil {
-		log.Println("[/admin/repo/edit]", err.Error())
-	}
-}
diff --git a/src/goit/repo.go b/src/goit/repo.go
index 32c95a0..13b2f52 100644
--- a/src/goit/repo.go
+++ b/src/goit/repo.go
@@ -22,6 +22,32 @@ type Repo struct {
 	IsPrivate   bool   `json:"is_private"`
 }
 
+func GetRepos() ([]Repo, error) {
+	repos := []Repo{}
+
+	rows, err := db.Query("SELECT id, owner_id, name, description, is_private FROM repos")
+	if err != nil {
+		return nil, err
+	}
+
+	defer rows.Close()
+
+	for rows.Next() {
+		r := Repo{}
+		if err := rows.Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.IsPrivate); err != nil {
+			return nil, err
+		}
+
+		repos = append(repos, r)
+	}
+
+	if rows.Err() != nil {
+		return nil, err
+	}
+
+	return repos, nil
+}
+
 func GetRepo(rid int64) (*Repo, error) {
 	r := &Repo{}
 
diff --git a/src/goit/user.go b/src/goit/user.go
index cbf110c..16c26d5 100644
--- a/src/goit/user.go
+++ b/src/goit/user.go
@@ -29,6 +29,32 @@ func HandleUserLogout(w http.ResponseWriter, r *http.Request) {
 	http.Redirect(w, r, "/", http.StatusFound)
 }
 
+func GetUsers() ([]User, error) {
+	users := []User{}
+
+	rows, err := db.Query("SELECT id, name, name_full, pass, pass_algo, salt, is_admin FROM users")
+	if err != nil {
+		return nil, err
+	}
+
+	defer rows.Close()
+
+	for rows.Next() {
+		u := User{}
+		if err := rows.Scan(&u.Id, &u.Name, &u.FullName, &u.Pass, &u.PassAlgo, &u.Salt, &u.IsAdmin); err != nil {
+			return nil, err
+		}
+
+		users = append(users, u)
+	}
+
+	if rows.Err() != nil {
+		return nil, err
+	}
+
+	return users, nil
+}
+
 func GetUser(id int64) (*User, error) {
 	u := User{}
 
@@ -72,10 +98,21 @@ func UserExists(name string) (bool, error) {
 	}
 }
 
+func CreateUser(user User) error {
+	if _, err := db.Exec(
+		"INSERT INTO users (name, name_full, pass, pass_algo, salt, is_admin) VALUES (?, ?, ?, ?, ?, ?)",
+		user.Name, user.FullName, user.Pass, user.PassAlgo, user.Salt, user.IsAdmin,
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func UpdateUser(uid int64, user User) error {
 	if _, err := db.Exec(
-		"UPDATE users SET name = ?, name_full = ? WHERE id = ?",
-		user.Name, user.FullName, uid,
+		"UPDATE users SET name = ?, name_full = ?, is_admin = ? WHERE id = ?",
+		user.Name, user.FullName, user.IsAdmin, uid,
 	); err != nil {
 		return err
 	}
diff --git a/src/main.go b/src/main.go
index fd36b89..3d96181 100644
--- a/src/main.go
+++ b/src/main.go
@@ -19,6 +19,7 @@ import (
 	"time"
 
 	"github.com/Jamozed/Goit/res"
+	"github.com/Jamozed/Goit/src/admin"
 	"github.com/Jamozed/Goit/src/goit"
 	"github.com/Jamozed/Goit/src/repo"
 	"github.com/Jamozed/Goit/src/user"
@@ -85,12 +86,12 @@ func main() {
 	h.Path("/user/sessions").Methods("GET", "POST").HandlerFunc(user.HandleSessions)
 	h.Path("/user/edit").Methods("GET", "POST").HandlerFunc(user.HandleEdit)
 	h.Path("/repo/create").Methods("GET", "POST").HandlerFunc(repo.HandleCreate)
-	h.Path("/admin").Methods("GET").HandlerFunc(goit.HandleAdminIndex)
-	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("/admin").Methods("GET").HandlerFunc(admin.HandleIndex)
+	h.Path("/admin/users").Methods("GET").HandlerFunc(admin.HandleUsers)
+	h.Path("/admin/user/create").Methods("GET", "POST").HandlerFunc(admin.HandleUserCreate)
+	h.Path("/admin/user/edit").Methods("GET", "POST").HandlerFunc(admin.HandleUserEdit)
+	h.Path("/admin/repos").Methods("GET").HandlerFunc(admin.HandleRepos)
+	h.Path("/admin/repo/edit").Methods("GET", "POST").HandlerFunc(admin.HandleRepoEdit)
 
 	h.Path("/{repo:.+(?:\\.git)$}").Methods("GET").HandlerFunc(redirectDotGit)
 	h.Path("/{repo}").Methods("GET").HandlerFunc(repo.HandleLog)
diff --git a/src/repo/edit.go b/src/repo/edit.go
index 2afa940..d423a30 100644
--- a/src/repo/edit.go
+++ b/src/repo/edit.go
@@ -106,6 +106,8 @@ func HandleEdit(w http.ResponseWriter, r *http.Request) {
 				return
 			} else if exists && data.Edit.Name != repo.Name {
 				data.Edit.Message = "Name \"" + data.Edit.Name + "\" is taken"
+			} else if len(data.Edit.Description) > 256 {
+				data.Edit.Message = "Description cannot exceed 256 characters"
 			} else if err := goit.UpdateRepo(repo.Id, goit.Repo{
 				Name: data.Edit.Name, Description: data.Edit.Description, IsPrivate: data.Edit.IsPrivate,
 			}); err != nil {
diff --git a/src/user/edit.go b/src/user/edit.go
index 1420054..7ef2739 100644
--- a/src/user/edit.go
+++ b/src/user/edit.go
@@ -56,7 +56,7 @@ func HandleEdit(w http.ResponseWriter, r *http.Request) {
 			} else if exists && data.Form.Name != user.Name {
 				data.MessageA = "Username \"" + data.Form.Name + "\" is taken"
 			} else if err := goit.UpdateUser(user.Id, goit.User{
-				Name: data.Form.Name, FullName: data.Form.FullName,
+				Name: data.Form.Name, FullName: data.Form.FullName, IsAdmin: user.IsAdmin,
 			}); err != nil {
 				log.Println("[/user/edit]", err.Error())
 				goit.HttpError(w, http.StatusInternalServerError)