summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorckrinitsin <101062646+ckrinitsin@users.noreply.github.com>2025-04-24 23:54:17 +0200
committerGitHub <noreply@github.com>2025-04-24 23:54:17 +0200
commitb75c7c6a9a040af03ef8b498c88c267bfa00aaf4 (patch)
tree8b30f8b1269fe985a1fe0d5cca8b771a6e7484ff
parent1e974f70a6c262d0b5db8b177ebb02b46446bfb0 (diff)
downloadshopping-list-b75c7c6a9a040af03ef8b498c88c267bfa00aaf4.tar.gz
shopping-list-b75c7c6a9a040af03ef8b498c88c267bfa00aaf4.zip
Authentication (#1)
* add login and register templates

* add jwt and gin-contrib dependencies

* add List database table

* add authentication

* add logout
-rw-r--r--authenticate/authenticate.go233
-rw-r--r--go.mod5
-rw-r--r--go.sum10
-rw-r--r--handlers/shopping_list.go81
-rw-r--r--main.go21
-rw-r--r--models/database.go20
-rw-r--r--templates/login.html169
-rw-r--r--templates/register.html173
-rw-r--r--templates/template.html12
9 files changed, 701 insertions, 23 deletions
diff --git a/authenticate/authenticate.go b/authenticate/authenticate.go
new file mode 100644
index 0000000..8862313
--- /dev/null
+++ b/authenticate/authenticate.go
@@ -0,0 +1,233 @@
+package authenticate
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/ckrinitsin/shopping-list/models"
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"github.com/golang-jwt/jwt/v5"
+	"golang.org/x/crypto/bcrypt"
+	"gorm.io/gorm"
+)
+
+func CheckAuth(c *gin.Context) {
+	session := sessions.Default(c)
+	token_session := session.Get("token")
+
+	if token_session == nil {
+		c.Redirect(http.StatusFound, "/login")
+		return
+	}
+
+	token_string, ok := token_session.(string)
+	if !ok {
+		c.Redirect(http.StatusFound, "/login")
+		return
+	}
+
+	token, err := jwt.Parse(token_string, func(token *jwt.Token) (any, error) {
+		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
+		}
+		return []byte(os.Getenv("SECRET")), nil
+	})
+
+	if err != nil || !token.Valid {
+		c.Redirect(http.StatusFound, "/login")
+		c.Error(err)
+		return
+	}
+
+	claims, ok := token.Claims.(jwt.MapClaims)
+	if !ok {
+		c.Redirect(http.StatusFound, "/login")
+		return
+	}
+
+	if float64(time.Now().Unix()) > claims["exp"].(float64) {
+		c.Redirect(http.StatusFound, "/login")
+		return
+	}
+
+	var list models.List
+	err = models.DB.
+		Model(&models.List{}).
+		Where("name = ?", claims["username"]).
+		First(&list).
+		Error
+	if err != nil {
+		c.Redirect(http.StatusFound, "/login")
+		return
+	}
+
+	c.Set("current_list", list)
+
+	c.Next()
+}
+
+func LoginGET(c *gin.Context) {
+	title := "Shopping List"
+
+	c.HTML(http.StatusOK, "login.html", gin.H{
+		"name":      title,
+		"error":     "",
+		"base_path": models.BasePath(),
+	})
+}
+
+func LoginPOST(c *gin.Context) {
+	username := strings.TrimSpace(c.PostForm("username"))
+	password := c.PostForm("password")
+
+	var list models.List
+	err := models.DB.
+		Model(&models.List{}).
+		Where("name = ?", username).
+		First(&list).
+		Error
+
+	if err == gorm.ErrRecordNotFound {
+		c.HTML(http.StatusBadRequest, "login.html", gin.H{
+			"error": "User does not exist",
+		})
+		return
+	} else if err != nil {
+		c.HTML(http.StatusInternalServerError, "login.html", gin.H{
+			"error": "Internal Server Error",
+		})
+		c.Error(err)
+		return
+	}
+
+	err = bcrypt.CompareHashAndPassword(list.Password, []byte(password))
+	if err == bcrypt.ErrMismatchedHashAndPassword {
+		c.HTML(http.StatusBadRequest, "login.html", gin.H{
+			"error": "Invalid username or password",
+		})
+		return
+	} else if err != nil {
+		c.HTML(http.StatusInternalServerError, "login.html", gin.H{
+			"error": "Internal Server Error",
+		})
+		c.Error(err)
+		return
+	}
+
+	session := sessions.Default(c)
+
+	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+		"username": username,
+		"exp":      time.Now().Add(time.Hour * 24 * 30).Unix(),
+	}).SignedString([]byte(os.Getenv("SECRET")))
+
+	if err != nil {
+		c.HTML(http.StatusInternalServerError, "login.html", gin.H{
+			"error": "Internal Server Error",
+		})
+		c.Error(err)
+		return
+	}
+
+	session.Set("token", token)
+	session.Save()
+
+	c.Redirect(http.StatusFound, "/")
+}
+
+func RegisterGET(c *gin.Context) {
+	title := "Shopping List"
+
+	c.HTML(http.StatusOK, "register.html", gin.H{
+		"name":      title,
+		"error":     "",
+		"base_path": models.BasePath(),
+	})
+}
+
+func RegisterPOST(c *gin.Context) {
+	username := strings.TrimSpace(c.PostForm("username"))
+	password := c.PostForm("password")
+	global_password := strings.TrimSpace(c.PostForm("global_password"))
+
+	if username == "" {
+		c.HTML(http.StatusBadRequest, "register.html", gin.H{
+			"error": "Invalid username",
+		})
+		return
+	}
+
+	if len(password) <= 0 && len(password) <= 72 {
+		c.HTML(http.StatusBadRequest, "register.html", gin.H{
+			"error": "Invalid password",
+		})
+		return
+	}
+
+	if global_password != os.Getenv("GLOBAL_PASSWORD") {
+		c.HTML(http.StatusBadRequest, "register.html", gin.H{
+			"error": "Global Password is wrong",
+		})
+		return
+	}
+
+	var count int64
+	err := models.DB.
+		Model(&models.List{}).
+		Where("name = ?", username).
+		Count(&count).
+		Error
+
+	if count > 0 {
+		c.HTML(http.StatusBadRequest, "register.html", gin.H{
+			"error": "User does exist already",
+		})
+		return
+	} else if err != gorm.ErrRecordNotFound && err != nil {
+		c.HTML(http.StatusInternalServerError, "register.html", gin.H{
+			"error": "Internal Server Error",
+		})
+		c.Error(err)
+		return
+	}
+
+	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+	if err != nil {
+		c.HTML(http.StatusInternalServerError, "register.html", gin.H{
+			"error": "Internal Server Error",
+		})
+		c.Error(err)
+		return
+	}
+
+	var list models.List
+	list = models.List{
+		Name:     username,
+		Password: hash,
+	}
+
+	err = models.DB.
+		Create(&list).
+		Error
+
+	if err != nil {
+		c.HTML(http.StatusInternalServerError, "register.html", gin.H{
+			"error": "Internal Server Error",
+		})
+		c.Error(err)
+		return
+	}
+
+	c.Redirect(http.StatusFound, "/login")
+}
+
+func Logout(c *gin.Context) {
+	session := sessions.Default(c)
+	session.Delete("token")
+	session.Save()
+	c.Redirect(http.StatusFound, "/login")
+}
diff --git a/go.mod b/go.mod
index f90c2b2..4af3420 100644
--- a/go.mod
+++ b/go.mod
@@ -8,12 +8,17 @@ require (
 	github.com/cloudwego/base64x v0.1.5 // indirect
 	github.com/cloudwego/iasm v0.2.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
+	github.com/gin-contrib/sessions v1.0.3 // indirect
 	github.com/gin-contrib/sse v1.1.0 // indirect
 	github.com/gin-gonic/gin v1.10.0 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.26.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
+	github.com/gorilla/context v1.1.2 // indirect
+	github.com/gorilla/securecookie v1.1.2 // indirect
+	github.com/gorilla/sessions v1.4.0 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
diff --git a/go.sum b/go.sum
index 421c093..57e213b 100644
--- a/go.sum
+++ b/go.sum
@@ -11,6 +11,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
 github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
+github.com/gin-contrib/sessions v1.0.3 h1:AZ4j0AalLsGqdrKNbbrKcXx9OJZqViirvNGsJTxcQps=
+github.com/gin-contrib/sessions v1.0.3/go.mod h1:5i4XMx4KPtQihnzxEqG9u1K446lO3G19jAi2GtbfsAI=
 github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
 github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
 github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@@ -23,7 +25,15 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
 github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
+github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
+github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
diff --git a/handlers/shopping_list.go b/handlers/shopping_list.go
index 959c309..a1b90ff 100644
--- a/handlers/shopping_list.go
+++ b/handlers/shopping_list.go
@@ -2,26 +2,29 @@ package shopping_list
 
 import (
 	"net/http"
-	"os"
 
 	"github.com/ckrinitsin/shopping-list/models"
 	"github.com/gin-gonic/gin"
 )
 
-func getBasePath() string {
-	basePath := os.Getenv("BASE_PATH")
-	if basePath == "" {
-		basePath = "/"
-	}
-
-	return basePath
-}
 
 func LoadElements(c *gin.Context) {
-	title := "Shopping List"
 	var entries []models.Entry
 
+	any_list, ok := c.Get("current_list")
+	if !ok {
+		c.String(http.StatusInternalServerError, "Internal Server Error")
+		return
+	}
+
+	list, ok := any_list.(models.List)
+	if !ok {
+		c.String(http.StatusInternalServerError, "Internal Server Error")
+		return
+	}
+
 	err := models.DB.
+		Where("list_name = ?", list.Name).
 		Order("checked asc").
 		Find(&entries).
 		Error
@@ -33,18 +36,31 @@ func LoadElements(c *gin.Context) {
 	}
 
 	c.HTML(http.StatusOK, "template.html", gin.H{
-		"name":      title,
+		"name":      list.Name,
 		"entries":   entries,
-		"base_path": getBasePath(),
+		"base_path": models.BasePath(),
 	})
 }
 
 func CreateEntry(c *gin.Context) {
 	value := c.PostForm("newItem")
 
+	any_list, ok := c.Get("current_list")
+	if !ok {
+		c.String(http.StatusInternalServerError, "Internal Server Error")
+		return
+	}
+
+	list, ok := any_list.(models.List)
+	if !ok {
+		c.String(http.StatusInternalServerError, "Internal Server Error")
+		return
+	}
+
 	entry := models.Entry{
 		Text:    value,
 		Checked: false,
+		ListName: list.Name,
 	}
 
 	err := models.DB.
@@ -56,21 +72,54 @@ func CreateEntry(c *gin.Context) {
 		return
 	}
 
-	c.Redirect(http.StatusFound, getBasePath() + "/")
+	c.Redirect(http.StatusFound, models.BasePath() + "/")
 }
 
 func DeleteEntries(c *gin.Context) {
-	models.DB.Delete(&models.Entry{}, "checked = 1")
+	any_list, ok := c.Get("current_list")
+	if !ok {
+		c.String(http.StatusInternalServerError, "Internal Server Error")
+		return
+	}
+
+	list, ok := any_list.(models.List)
+	if !ok {
+		c.String(http.StatusInternalServerError, "Internal Server Error")
+		return
+	}
 
-	c.Redirect(http.StatusFound, getBasePath() + "/")
+	err := models.DB.
+		Where("list_name = ?", list.Name).
+		Delete(&models.Entry{}, "checked = 1").
+		Error
+
+	if err != nil {
+		c.String(http.StatusInternalServerError, "Internal Server Error")
+		return
+	}
+
+	c.Redirect(http.StatusFound, models.BasePath() + "/")
 }
 
 func ToggleEntry(c *gin.Context) {
 	id := c.PostForm("id")
 
+	any_list, ok := c.Get("current_list")
+	if !ok {
+		c.String(http.StatusInternalServerError, "Internal Server Error")
+		return
+	}
+
+	list, ok := any_list.(models.List)
+	if !ok {
+		c.String(http.StatusInternalServerError, "Internal Server Error")
+		return
+	}
+
 	var entry models.Entry
 
 	err := models.DB.
+		Where("list_name = ?", list.Name).
 		First(&entry, id).
 		Error
 
@@ -89,5 +138,5 @@ func ToggleEntry(c *gin.Context) {
 		return
 	}
 
-	c.Redirect(http.StatusFound, getBasePath() + "/")
+	c.Redirect(http.StatusFound, models.BasePath() + "/")
 }
diff --git a/main.go b/main.go
index 0553af0..ef20704 100644
--- a/main.go
+++ b/main.go
@@ -4,9 +4,13 @@ import (
 	"embed"
 	"html/template"
 	"net/http"
+	"os"
 
+	"github.com/ckrinitsin/shopping-list/authenticate"
 	"github.com/ckrinitsin/shopping-list/handlers"
 	"github.com/ckrinitsin/shopping-list/models"
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-contrib/sessions/cookie"
 	"github.com/gin-gonic/gin"
 )
 
@@ -21,16 +25,25 @@ func main() {
 	tmpl := template.Must(template.ParseFS(templatesFS, "templates/*"))
 	r.SetHTMLTemplate(tmpl)
 
+	store := cookie.NewStore([]byte(os.Getenv("SECRET")))
+	r.Use(sessions.Sessions("session", store))
+
 	r.GET("/health", func(c *gin.Context) {
 		c.JSON(http.StatusOK, gin.H{
 			"health-check": "passed",
 		})
 	})
 
-	r.GET("/", shopping_list.LoadElements)
-	r.POST("/create", shopping_list.CreateEntry)
-	r.POST("/delete", shopping_list.DeleteEntries)
-	r.POST("/toggle", shopping_list.ToggleEntry)
+	r.POST("/login", authenticate.LoginPOST)
+	r.GET("/login", authenticate.LoginGET)
+	r.POST("/register", authenticate.RegisterPOST)
+	r.GET("/register", authenticate.RegisterGET)
+	r.POST("/logout", authenticate.Logout)
+
+	r.GET("/", authenticate.CheckAuth, shopping_list.LoadElements)
+	r.POST("/create", authenticate.CheckAuth, shopping_list.CreateEntry)
+	r.POST("/delete", authenticate.CheckAuth, shopping_list.DeleteEntries)
+	r.POST("/toggle", authenticate.CheckAuth, shopping_list.ToggleEntry)
 
 	r.Run()
 }
diff --git a/models/database.go b/models/database.go
index 2daaa1e..ffc8a47 100644
--- a/models/database.go
+++ b/models/database.go
@@ -1,18 +1,28 @@
 package models
 
 import (
+	"os"
 	"time"
 
 	"gorm.io/driver/sqlite"
 	"gorm.io/gorm"
 )
 
+type List struct {
+	Name      string `gorm:"primaryKey"`
+	Password  []byte
+	CreatedAt time.Time
+	UpdatedAt time.Time
+	Entries   []Entry
+}
+
 type Entry struct {
 	ID        uint `gorm:"primaryKey"`
 	Text      string
 	Checked   bool
 	CreatedAt time.Time
 	UpdatedAt time.Time
+	ListName  string
 }
 
 var DB *gorm.DB
@@ -24,7 +34,17 @@ func ConnectDatabase() {
 		panic("Failed to connect to database!")
 	}
 
+	db.AutoMigrate(&List{})
 	db.AutoMigrate(&Entry{})
 
 	DB = db
 }
+
+func BasePath() string {
+	basePath := os.Getenv("BASE_PATH")
+	if basePath == "" {
+		basePath = "/"
+	}
+
+	return basePath
+}
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000..ab4801d
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title> Login </title>
+    <style>
+        html, body {
+            width: 100%;
+            height: 100%;
+            margin: 0;
+            overflow: hidden;
+        }
+
+        body {
+            font-family: Arial, sans-serif;
+            background-color: #2d353b;
+            color: #d3c6aa;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+        }
+
+        .container {
+            padding: 2rem;
+            width: 300px;
+            max-height: 100%;
+        }
+
+        .checklist {
+            max-height: 500px;
+            overflow-y: auto;
+            padding: 10px;
+        }
+
+        h1 {
+            text-align: center;
+        }
+
+        ul {
+            list-style-type: none;
+            padding: 0;
+        }
+
+        li {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin: 0.5rem 0;
+            padding: 0.5rem;
+            border: 1px solid #d3c6aa;
+            border-radius: 4px;
+            height: 25px;
+        }
+
+        li.selected {
+            text-decoration: line-through;
+            color: #475258;
+            border: 1px solid #475258;
+        }
+
+        button {
+            display: block;
+            padding: 10px 20px;
+            border: none;
+            background-color: #d3c6aa;
+            color: #2d353b;
+            border-radius: 4px;
+            cursor: pointer;
+            width: 100%
+        }
+
+        .input-container{
+            position:relative;
+            margin-bottom:25px;
+            margin-top:25px;
+        }
+        .input-container label{
+            position:absolute;
+            top:0px;
+            left:0px;
+            font-size:16px;
+            color:#fff;	
+            pointer-event:none;
+            transition: all 0.5s ease-in-out;
+        }
+        .input-container input{
+          border:0;
+          border-bottom:1px solid #555;
+          background:transparent;
+          width:100%;
+          padding:8px 0 5px 0;
+          font-size:16px;
+          color:#d3c6aa;
+        }
+        .input-container input:focus{
+         border:none;	
+         outline:none;
+         border-bottom:1px solid #d3c6aa;	
+        }
+        .input-container input:focus ~ label,
+        .input-container input:valid ~ label{
+            top:-12px;
+            font-size:12px;
+            
+        }
+
+        /* settings for mini phones */
+        @media (max-width: 280px) {
+          h1 {
+            font-size: 0;
+            height: 0;
+            margin: 0;
+          }
+          li {
+            height: 18px;
+            margin: 0.3rem 0;
+          }
+          .container {
+            width: 268px; /* Scale font sizes */
+            height: 100%;
+            margin: 0;
+          }
+          .checklist {
+            height: 300px;
+            margin: 0;
+            padding: 3px;
+            padding-top: 0;
+          }
+          .input-container {
+            margin: 6px;
+          }
+          .delete-button {
+            width: 170px;
+          }
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <h1>Login</h1>
+
+        <p style="color: #e67e80;">
+            {{ .error }} 
+        </p>
+
+        <form action="{{ .base_path }}login" method="POST">
+            <div class="input-container">
+                Username:
+                <input type="text" id="newItem" name="username" tabindex="0"/>
+
+            </div>
+            <div class="input-container">
+                Password:
+                <input type="password" id="newItem" name="password" tabindex="0"/>
+            </div>
+            <button class="delete-button"> Login </button>
+        </form>
+
+        <form action="{{ .base_path }}register" method="GET">
+            <div class="input-container">
+                <button class="delete-button"> Register </button>
+            </div>
+        </form>
+
+    </div>
+</body>
+</html>
diff --git a/templates/register.html b/templates/register.html
new file mode 100644
index 0000000..f4b582c
--- /dev/null
+++ b/templates/register.html
@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title> Register</title>
+    <style>
+        html, body {
+            width: 100%;
+            height: 100%;
+            margin: 0;
+            overflow: hidden;
+        }
+
+        body {
+            font-family: Arial, sans-serif;
+            background-color: #2d353b;
+            color: #d3c6aa;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+        }
+
+        .container {
+            padding: 2rem;
+            width: 300px;
+            max-height: 100%;
+        }
+
+        .checklist {
+            max-height: 500px;
+            overflow-y: auto;
+            padding: 10px;
+        }
+
+        h1 {
+            text-align: center;
+        }
+
+        ul {
+            list-style-type: none;
+            padding: 0;
+        }
+
+        li {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin: 0.5rem 0;
+            padding: 0.5rem;
+            border: 1px solid #d3c6aa;
+            border-radius: 4px;
+            height: 25px;
+        }
+
+        li.selected {
+            text-decoration: line-through;
+            color: #475258;
+            border: 1px solid #475258;
+        }
+
+        button {
+            display: block;
+            padding: 10px 20px;
+            border: none;
+            background-color: #d3c6aa;
+            color: #2d353b;
+            border-radius: 4px;
+            cursor: pointer;
+            width: 100%
+        }
+
+        .input-container{
+            position:relative;
+            margin-bottom:25px;
+            margin-top:25px;
+        }
+        .input-container label{
+            position:absolute;
+            top:0px;
+            left:0px;
+            font-size:16px;
+            color:#fff;	
+            pointer-event:none;
+            transition: all 0.5s ease-in-out;
+        }
+        .input-container input{
+          border:0;
+          border-bottom:1px solid #555;
+          background:transparent;
+          width:100%;
+          padding:8px 0 5px 0;
+          font-size:16px;
+          color:#d3c6aa;
+        }
+        .input-container input:focus{
+         border:none;	
+         outline:none;
+         border-bottom:1px solid #d3c6aa;	
+        }
+        .input-container input:focus ~ label,
+        .input-container input:valid ~ label{
+            top:-12px;
+            font-size:12px;
+            
+        }
+
+        /* settings for mini phones */
+        @media (max-width: 280px) {
+          h1 {
+            font-size: 0;
+            height: 0;
+            margin: 0;
+          }
+          li {
+            height: 18px;
+            margin: 0.3rem 0;
+          }
+          .container {
+            width: 268px; /* Scale font sizes */
+            height: 100%;
+            margin: 0;
+          }
+          .checklist {
+            height: 300px;
+            margin: 0;
+            padding: 3px;
+            padding-top: 0;
+          }
+          .input-container {
+            margin: 6px;
+          }
+          .delete-button {
+            width: 170px;
+          }
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <h1>Register</h1>
+
+        <p style="color: #e67e80;">
+            {{ .error }} 
+        </p>
+
+        <form action="{{ .base_path }}register" method="POST">
+            <div class="input-container">
+                Username:
+                <input type="text" id="newItem" name="username" tabindex="0"/>
+
+            </div>
+            <div class="input-container">
+                Password:
+                <input type="password" id="newItem" name="password" tabindex="0"/>
+            </div>
+            <div class="input-container">
+                Global Password:
+                <input type="password" id="newItem" name="global_password" tabindex="0"/>
+            </div>
+            <button class="delete-button"> Register </button>
+        </form>
+
+        <form action="{{ .base_path }}login" method="GET">
+            <div class="input-container">
+                <button class="delete-button"> Login </button>
+            </div>
+        </form>
+
+    </div>
+</body>
+</html>
diff --git a/templates/template.html b/templates/template.html
index 7548624..b92f716 100644
--- a/templates/template.html
+++ b/templates/template.html
@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title> {{ .name }}</title>
+    <title>{{ .name }} - List</title>
     <style>
         html, body {
             width: 100%;
@@ -166,7 +166,7 @@
 </head>
 <body>
     <div class="container">
-        <h1>{{ .name }}</h1>
+        <h1>{{ .name }} - List</h1>
         <ul class="checklist" id="checklist">
             {{ range .entries }}
                 {{ if .Checked }}
@@ -190,10 +190,16 @@
             </div>
         </form>
 
-            <form action="{{ .base_path }}delete" method="POST" onsubmit="return confirm('Are you sure you want to delete all checked entries?')">
+        <form action="{{ .base_path }}delete" method="POST" onsubmit="return confirm('Are you sure you want to delete all checked entries?')">
             <button type="submit" class="delete-button">Delete Selected Items</button>
         </form>
 
+        <form action="{{ .base_path }}logout" method="POST" onsubmit="return confirm('Are you sure you want to logout?')">
+            <div class="input-container">
+                <button type="submit" class="delete-button">Logout</button>
+            </div>
+        </form>
+
     </div>
 </body>
 </html>