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 05:11:15
Commit0893c1e6ee7e521b4f36ee08c69daf4ce211cfd3
Parenta0ba6ae56e9aa32404aeaa74ab9792b3525eb81b

Integrate UID into session tokens

Diffstat

M main.go | 2 +-
A res/admin/index.html | 16 ++++++++++++++++
M res/admin/repo_edit.html | 38 ++++++++++++++++++++------------------
M res/admin/repos.html | 10 ++++++++--
M res/admin/user_create.html | 28 +++++++++++++++-------------
M res/admin/user_edit.html | 32 +++++++++++++++++---------------
M res/admin/users.html | 10 ++++++++--
M res/base/header.html | 2 +-
M res/res.go | 3 +++
M src/admin.go | 38 ++++++++++++++++++++++++++------------
M src/auth.go | 140 ++++++++++++++++++++++++++++++++++++++++++++------------------------------------
M src/http.go | 1 +
M src/repo.go | 27 +++++++++++++++++++--------
M src/user.go | 32 ++++++++++++++++----------------
M src/util/util.go | 9 +++++----

15 files changed, 233 insertions, 155 deletions

diff --git a/main.go b/main.go
index f3db37f..7debc01 100644
--- a/main.go
+++ b/main.go
@@ -27,7 +27,7 @@ func main() {
 	h.Path("/user/login").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogin)
 	h.Path("/user/logout").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogout)
 	h.Path("/repo/create").Methods("GET", "POST").HandlerFunc(goit.HandleRepoCreate)
-	// h.Path("/admin").Methods("GET").HandlerFunc()
+	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)
diff --git a/res/admin/index.html b/res/admin/index.html
new file mode 100644
index 0000000..257aed7
--- /dev/null
+++ b/res/admin/index.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<head>{{template "base/head" .}}</head>
+<body>
+	<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">Repos</a>
+		</td></tr>
+	</table>
+</body>
diff --git a/res/admin/repo_edit.html b/res/admin/repo_edit.html
index 7556e12..43d177e 100644
--- a/res/admin/repo_edit.html
+++ b/res/admin/repo_edit.html
@@ -1,22 +1,24 @@
 <!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>
+	<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">
+		</form>
+		<span>{{.Message}}</span>
+	</main>
 </body>
diff --git a/res/admin/repos.html b/res/admin/repos.html
index db8b797..5458c8c 100644
--- a/res/admin/repos.html
+++ b/res/admin/repos.html
@@ -4,11 +4,17 @@
 	<header>
 		<table>
 			<tr>
-				<td><img src = "/static/favicon.png" style="max-height: 24px"></td>
+				<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">Repos</a>
+			</td></tr>
 		</table>
-	</header>
+	</header><hr>
 	<main>
 		<table>
 			<thead>
diff --git a/res/admin/user_create.html b/res/admin/user_create.html
index c19b879..cea4814 100644
--- a/res/admin/user_create.html
+++ b/res/admin/user_create.html
@@ -1,17 +1,19 @@
 <!DOCTYPE html>
 <head>{{template "base/head" .}}</head>
 <body>
-	<h1>{{.Title}}</h1>
-	<form action="/admin/user/create" method="post">
-		<label for="username">Username:</label>
-		<input type="text" name="username"><br>
-		<label for="fullname">Full Name:</label>
-		<input type="text" name="fullname"><br>
-		<label for="password">Password:</label>
-		<input type="password" name="password"><br>
-		<label for="admin">Admin:</label>
-		<input type="checkbox" name="admin" value="true"><br>
-		<input type="submit" value="Create">
-	</form>
-	<span>{{.Message}}</span>
+	<main>
+		<h1>{{.Title}}</h1>
+		<form action="/admin/user/create" method="post">
+			<label for="username">Username:</label>
+			<input type="text" name="username"><br>
+			<label for="fullname">Full Name:</label>
+			<input type="text" name="fullname"><br>
+			<label for="password">Password:</label>
+			<input type="password" name="password"><br>
+			<label for="admin">Admin:</label>
+			<input type="checkbox" name="admin" value="true"><br>
+			<input type="submit" value="Create">
+		</form>
+		<span>{{.Message}}</span>
+	</main>
 </body>
diff --git a/res/admin/user_edit.html b/res/admin/user_edit.html
index 877a7b7..13ca379 100644
--- a/res/admin/user_edit.html
+++ b/res/admin/user_edit.html
@@ -1,19 +1,21 @@
 <!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>
+	<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">
+		</form>
+		<span>{{.Message}}</span>
+	</main>
 </body>
diff --git a/res/admin/users.html b/res/admin/users.html
index b1d88bf..f224ae1 100644
--- a/res/admin/users.html
+++ b/res/admin/users.html
@@ -4,11 +4,17 @@
 	<header>
 		<table>
 			<tr>
-				<td><img src = "/static/favicon.png" style="max-height: 24px"></td>
+				<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">Repos</a>
+			</td></tr>
 		</table>
-	</header>
+	</header><hr>
 	<main>
 		<table>
 			<thead>
diff --git a/res/base/header.html b/res/base/header.html
index 1645db9..da113bb 100644
--- a/res/base/header.html
+++ b/res/base/header.html
@@ -13,7 +13,7 @@
 				| <a href="/admin">Admin</a>
 			{{end}}
 			{{if .Auth}}
-				| <a href="/user/logout">Logout</a>
+				| <a href="/user/logout">Logout</a>{{if .Username}} ({{.Username}}){{end}}
 			{{else}}
 				| <a href="/user/login">Login</a>
 			{{end}}
diff --git a/res/res.go b/res/res.go
index f8a1f73..837f577 100644
--- a/res/res.go
+++ b/res/res.go
@@ -14,6 +14,9 @@ var BaseHead string
 //go:embed base/header.html
 var BaseHeader string
 
+//go:embed admin/index.html
+var AdminIndex string
+
 //go:embed admin/users.html
 var AdminUsers string
 
diff --git a/src/admin.go b/src/admin.go
index e646691..c14579a 100644
--- a/src/admin.go
+++ b/src/admin.go
@@ -15,8 +15,19 @@ import (
 	"github.com/dustin/go-humanize"
 )
 
+func HandleAdminIndex(w http.ResponseWriter, r *http.Request) {
+	if _, admin, _ := AuthCookieAdmin(r); !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, _ := AuthHttpAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(r); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -34,7 +45,7 @@ func HandleAdminUsers(w http.ResponseWriter, r *http.Request) {
 	data := struct {
 		Title string
 		Users []row
-	}{Title: "Users"}
+	}{Title: "Admin - Users"}
 
 	for rows.Next() {
 		d := User{}
@@ -61,12 +72,12 @@ func HandleAdminUsers(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthHttpAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(r); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
 
-	data := struct{ Title, Message string }{"Create User", ""}
+	data := struct{ Title, Message string }{"Admin - Create User", ""}
 
 	if r.Method == http.MethodPost {
 		username := strings.ToLower(r.FormValue("username"))
@@ -106,12 +117,12 @@ func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthHttpAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(r); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
 
-	id, err := strconv.ParseUint(r.URL.Query().Get("user"), 10, 64)
+	id, err := strconv.ParseInt(r.URL.Query().Get("user"), 10, 64)
 	if err != nil {
 		HttpError(w, http.StatusNotFound)
 		return
@@ -130,7 +141,10 @@ 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: "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"))
@@ -181,7 +195,7 @@ func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminRepos(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthHttpAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(r); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -199,7 +213,7 @@ func HandleAdminRepos(w http.ResponseWriter, r *http.Request) {
 	data := struct {
 		Title string
 		Repos []row
-	}{Title: "Repositories"}
+	}{Title: "Admin - Repositories"}
 
 	for rows.Next() {
 		d := Repo{}
@@ -236,12 +250,12 @@ func HandleAdminRepos(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminRepoEdit(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthHttpAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(r); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
 
-	id, err := strconv.ParseUint(r.URL.Query().Get("repo"), 10, 64)
+	id, err := strconv.ParseInt(r.URL.Query().Get("repo"), 10, 64)
 	if err != nil {
 		HttpError(w, http.StatusNotFound)
 		return
@@ -261,7 +275,7 @@ func HandleAdminRepoEdit(w http.ResponseWriter, r *http.Request) {
 		Title, Id, Owner, Name, Description, Message string
 		IsPrivate                                    bool
 	}{
-		Title: "Edit Repository", Id: fmt.Sprint(repo.Id), Name: repo.Name, Description: repo.Description,
+		Title: "Admin - Edit Repository", Id: fmt.Sprint(repo.Id), Name: repo.Name, Description: repo.Description,
 		IsPrivate: repo.IsPrivate,
 	}
 
diff --git a/src/auth.go b/src/auth.go
index 5888e02..8a28428 100644
--- a/src/auth.go
+++ b/src/auth.go
@@ -9,110 +9,124 @@ import (
 	"encoding/base64"
 	"fmt"
 	"log"
-	"math"
 	"net/http"
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/Jamozed/Goit/src/util"
 	"golang.org/x/crypto/argon2"
 )
 
-type session struct {
-	id     uint64
-	expiry time.Time
+type Session struct {
+	Token, Ip string
+	Expiry    time.Time
 }
 
-var sessions = map[string]session{}
+var Sessions = map[int64]map[string]Session{}
 
-/* Hash a password with a salt using Argon2. */
-func Hash(pass string, salt []byte) []byte {
-	return argon2.IDKey([]byte(pass), salt, 3, 64*1024, 4, 32)
-}
-
-/* Generate a random Base64 salt. */
-func Salt() ([]byte, error) {
-	b := make([]byte, 16)
+func NewSession(uid int64, ip string, expiry time.Time) (Session, error) {
+	b := make([]byte, 24)
 	if _, err := rand.Read(b); err != nil {
-		return nil, err
+		return Session{}, err
 	}
 
-	return b, nil
-}
-
-func NewSession(id uint64, expiry time.Time) (string, error) {
-	b := make([]byte, 24)
-	if _, err := rand.Read(b); err != nil {
-		return "", err
+	if Sessions[uid] == nil {
+		Sessions[uid] = map[string]Session{}
 	}
 
-	s := base64.StdEncoding.EncodeToString(b)
-	sessions[s] = session{id, expiry}
-	return s, nil
+	t := base64.StdEncoding.EncodeToString(b)
+	Sessions[uid][t] = Session{t, ip, expiry}
+	return Sessions[uid][t], nil
 }
 
-func EndSession(s string) {
-	delete(sessions, s)
+func EndSession(id int64, token string) {
+	delete(Sessions[id], token)
+	if len(Sessions[id]) == 0 {
+		delete(Sessions, id)
+	}
 }
 
-func Auth(s string) (bool, uint64) {
-	if v, ok := sessions[s]; ok {
-		if v.expiry.After(time.Now()) {
-			return true, v.id
-		} else {
-			delete(sessions, s)
+func CleanupSessions() {
+	var n uint64 = 0
+
+	for k, v := range Sessions {
+		for k1, v1 := range v {
+			if v1.Expiry.Before(time.Now()) {
+				EndSession(k, k1)
+				n += 1
+			}
 		}
 	}
 
-	return false, math.MaxUint64
-}
-
-func AuthHttp(r *http.Request) (bool, uint64) {
-	if c := util.Cookie(r, "session"); c != nil {
-		return Auth(c.Value)
+	if n > 0 {
+		log.Println("[Cleanup] cleaned up", n, "expired sessions")
 	}
+}
 
-	return false, math.MaxUint64
+func SetSessionCookie(w http.ResponseWriter, uid int64, s Session) {
+	http.SetCookie(w, &http.Cookie{
+		Name: "session", Value: fmt.Sprint(uid) + "." + s.Token, Path: "/", Expires: s.Expiry,
+	})
 }
 
-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
+func GetSessionCookie(r *http.Request) (int64, Session) {
+	if c := util.Cookie(r, "session"); c != nil {
+		ss := strings.SplitN(c.Value, ".", 2)
+		if len(ss) != 2 {
+			return -1, Session{}
 		}
 
-		return true, false, uid
+		id, err := strconv.ParseInt(ss[0], 10, 64)
+		if err != nil {
+			return -1, Session{}
+		}
+
+		return id, Sessions[id][ss[1]]
 	}
 
-	return false, false, math.MaxUint64
+	return -1, Session{}
 }
 
-func SessionCookie(r *http.Request) string {
-	if c := util.Cookie(r, "session"); c != nil {
-		return c.Value
+func EndSessionCookie(w http.ResponseWriter) {
+	http.SetCookie(w, &http.Cookie{Name: "session", Path: "/", MaxAge: -1})
+}
+
+func AuthCookie(r *http.Request) (auth bool, uid int64) {
+	if uid, s := GetSessionCookie(r); s != (Session{}) {
+		if s.Expiry.After(time.Now()) {
+			return true, uid
+		}
+
+		EndSession(uid, s.Token)
 	}
 
-	return ""
+	return false, -1
 }
 
-func GetSessions() (s string) {
-	for k, v := range sessions {
-		s += fmt.Sprint(k, v.id, v.expiry)
+func AuthCookieAdmin(r *http.Request) (auth bool, admin bool, uid int64) {
+	if ok, uid := AuthCookie(r); ok {
+		if user, err := GetUser(uid); err == nil && user.IsAdmin {
+			return true, true, uid
+		}
+
+		return true, false, uid
 	}
 
-	return s
+	return false, false, -1
 }
 
-func CleanupSessions() {
-	n := 0
+/* Hash a password with a salt using Argon2. */
+func Hash(pass string, salt []byte) []byte {
+	return argon2.IDKey([]byte(pass), salt, 3, 64*1024, 4, 32)
+}
 
-	for k, v := range sessions {
-		if v.expiry.Before(time.Now()) {
-			delete(sessions, k)
-			n += 1
-		}
+/* Generate a random Base64 salt. */
+func Salt() ([]byte, error) {
+	b := make([]byte, 16)
+	if _, err := rand.Read(b); err != nil {
+		return nil, err
 	}
 
-	if n > 0 {
-		log.Println("[Sessions] Cleaned up", n, "expired sessions")
-	}
+	return b, nil
 }
diff --git a/src/http.go b/src/http.go
index 29e858e..918646e 100644
--- a/src/http.go
+++ b/src/http.go
@@ -19,6 +19,7 @@ func init() {
 	template.Must(tmpl.New("base/head").Parse(res.BaseHead))
 	template.Must(tmpl.New("base/header").Parse(res.BaseHeader))
 
+	template.Must(tmpl.New("admin/index").Parse(res.AdminIndex))
 	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))
diff --git a/src/repo.go b/src/repo.go
index eae9078..0f7b260 100644
--- a/src/repo.go
+++ b/src/repo.go
@@ -22,8 +22,8 @@ import (
 )
 
 type Repo struct {
-	Id            uint64
-	OwnerId       uint64
+	Id            int64
+	OwnerId       int64
 	Name          string
 	Description   string
 	DefaultBranch string
@@ -31,7 +31,14 @@ type Repo struct {
 }
 
 func HandleIndex(w http.ResponseWriter, r *http.Request) {
-	auth, admin, uid := AuthHttpAdmin(r)
+	auth, admin, uid := AuthCookieAdmin(r)
+
+	user, err := GetUser(uid)
+	if err != nil {
+		log.Println("[/]", err.Error())
+		HttpError(w, http.StatusInternalServerError)
+		return
+	}
 
 	if rows, err := db.Query("SELECT id, owner_id, name, description, is_private FROM repos"); err != nil {
 		log.Println("[/]", err.Error())
@@ -41,11 +48,15 @@ func HandleIndex(w http.ResponseWriter, r *http.Request) {
 
 		type row struct{ Name, Description, Owner, Visibility, LastCommit string }
 		data := struct {
-			Title       string
-			Admin, Auth bool
-			Repos       []row
+			Title, Username string
+			Admin, Auth     bool
+			Repos           []row
 		}{Title: "Repositories", Admin: admin, Auth: auth}
 
+		if user != nil {
+			data.Username = user.Name
+		}
+
 		for rows.Next() {
 			d := Repo{}
 			if err := rows.Scan(&d.Id, &d.OwnerId, &d.Name, &d.Description, &d.IsPrivate); err != nil {
@@ -75,7 +86,7 @@ func HandleIndex(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleRepoCreate(w http.ResponseWriter, r *http.Request) {
-	if ok, uid := AuthHttp(r); !ok {
+	if ok, uid := AuthCookie(r); !ok {
 		HttpError(w, http.StatusUnauthorized)
 	} else if r.Method == http.MethodPost {
 		name := r.FormValue("reponame")
@@ -216,7 +227,7 @@ func HandleRepoRefs(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func GetRepo(id uint64) (*Repo, error) {
+func GetRepo(id int64) (*Repo, error) {
 	r := &Repo{}
 
 	if err := db.QueryRow(
diff --git a/src/user.go b/src/user.go
index 77c11a9..cd6bec8 100644
--- a/src/user.go
+++ b/src/user.go
@@ -16,7 +16,7 @@ import (
 )
 
 type User struct {
-	Id       uint64
+	Id       int64
 	Name     string
 	FullName string
 	Pass     []byte
@@ -28,7 +28,7 @@ type User struct {
 var reserved []string = []string{"admin", "repo", "static", "user"}
 
 func HandleUserLogin(w http.ResponseWriter, r *http.Request) {
-	if ok, _ := AuthHttp(r); ok {
+	if ok, _ := AuthCookie(r); ok {
 		http.Redirect(w, r, "/", http.StatusFound)
 		return
 	}
@@ -56,30 +56,30 @@ func HandleUserLogin(w http.ResponseWriter, r *http.Request) {
 			return
 		} else if !bytes.Equal(Hash(password, u.Salt), u.Pass) {
 			data.Message = "Invalid credentials"
+		} else if s, err := NewSession(u.Id, r.RemoteAddr, time.Now().Add(15*time.Minute)); err != nil {
+			log.Println("[User:Login:Session]", err.Error())
+			HttpError(w, http.StatusInternalServerError)
+			return
 		} else {
-			expiry := time.Now().Add(15 * time.Minute)
-			if s, err := NewSession(u.Id, expiry); err != nil {
-				log.Println("[User:Login:Session]", err.Error())
-				HttpError(w, http.StatusInternalServerError)
-				return
-			} else {
-				http.SetCookie(w, &http.Cookie{Name: "session", Value: s, Path: "/", Expires: expiry})
-				http.Redirect(w, r, "/", http.StatusFound)
-				return
-			}
+			SetSessionCookie(w, u.Id, s)
+			http.Redirect(w, r, "/", http.StatusFound)
+			return
 		}
 	}
 
-	tmpl.ExecuteTemplate(w, "user_login", data)
+	if err := tmpl.ExecuteTemplate(w, "user_login", data); err != nil {
+		log.Println("[/user/login]", err.Error())
+	}
 }
 
 func HandleUserLogout(w http.ResponseWriter, r *http.Request) {
-	EndSession(SessionCookie(r))
-	http.SetCookie(w, &http.Cookie{Name: "session", Path: "/", MaxAge: -1})
+	id, s := GetSessionCookie(r)
+	EndSession(id, s.Token)
+	EndSessionCookie(w)
 	http.Redirect(w, r, "/", http.StatusFound)
 }
 
-func GetUser(id uint64) (*User, error) {
+func GetUser(id int64) (*User, error) {
 	u := User{}
 
 	if err := db.QueryRow(
diff --git a/src/util/util.go b/src/util/util.go
index 065fd85..bc3e587 100644
--- a/src/util/util.go
+++ b/src/util/util.go
@@ -26,11 +26,12 @@ func SliceContains[T comparable](s []T, e T) bool {
 	return false
 }
 
-/* Return the named cookie or nil if not found. */
+/* Return the named cookie or nil if not found or invalid. */
 func Cookie(r *http.Request, name string) *http.Cookie {
-	if c, err := r.Cookie(name); err != nil {
-		return nil
-	} else {
+	c, err := r.Cookie(name)
+	if err == nil && c.Valid() == nil {
 		return c
 	}
+
+	return nil
 }