From 624877eb2002638824ffd4f42fc171fd8e48b523 Mon Sep 17 00:00:00 2001
From: Radon <joshuacasady@gmail.com>
Date: Thu, 23 Jan 2025 19:37:15 -0600
Subject: [PATCH] overhaul changes initial commit

---
 config.json        |   9 +-
 db/db.go           | 132 ++++++++++++++----------------
 go.mod             |   2 +
 go.sum             |   2 +
 public/index.js    |  32 ++++++--
 public/login.html  | 178 ++++++++++++++++++++++++++++++++++++++++
 public/signup.html | 186 +++++++++++++++++++++++++++++++++++++++++
 readme.md          |   9 +-
 srv/handle.go      | 200 ++++++++++++++++++++++++++++++++-------------
 srv/srv.go         |  41 ++++++++--
 10 files changed, 642 insertions(+), 149 deletions(-)
 create mode 100644 public/login.html
 create mode 100644 public/signup.html

diff --git a/config.json b/config.json
index bb9b9f7..069d3bb 100644
--- a/config.json
+++ b/config.json
@@ -4,13 +4,16 @@
 		"port": 8080
 	},
 	"paths": {
-		"databasePath": "/home/radon/Documents/chattest.db",
+		"databasePath": "/home/radon/Documents/chat.db",
 		"indexJsPath": "./public/index.js",
 		"indexCssPath": "./public/style.css",
-		"indexHtmlPath": "./public/index.html"
+		"indexHtmlPath": "./public/index.html",
+		"signupHtmlPath": "./public/signup.html",
+		"loginHtmlPath": "./public/login.html"
 	},
 	"options": {
 		"messageMaxAge": 259200,
-		"nameMaxLength": 32
+		"nameMaxLength": 32,
+		"messagesPerPage": 10
 	}
 }
diff --git a/db/db.go b/db/db.go
index c443fbf..3f03678 100644
--- a/db/db.go
+++ b/db/db.go
@@ -4,20 +4,20 @@ import (
 	"database/sql"
 	"fmt"
 	_ "github.com/mattn/go-sqlite3"
+	"golang.org/x/crypto/bcrypt"
 	"strconv"
 	"time"
 )
 
 type User struct {
-	Id        string
-	Username  string
-	IpAddress string
-	Timezone  string
+	Id             string
+	Username       string
+	Timezone       string
+	HashedPassword string
 }
 
 type Message struct {
 	Id             string
-	SenderIp       string
 	SenderUsername string
 	Content        string
 	Timestamp      string
@@ -45,7 +45,7 @@ func (db *Database) Close() {
 func (db *Database) DbCreateTableMessages() {
 	stmt := `CREATE TABLE IF NOT EXISTS messages (
 		id INTEGER PRIMARY KEY AUTOINCREMENT,
-		ip_address TEXT NOT NULL,
+		username TEXT NOT NULL,
 		content TEXT NOT NULL,
 		edited INTEGER DEFAULT 0,
 		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@@ -56,61 +56,59 @@ func (db *Database) DbCreateTableMessages() {
 func (db *Database) DbCreateTableUsers() {
 	stmt := `CREATE TABLE IF NOT EXISTS users (
 		id INTEGER PRIMARY KEY AUTOINCREMENT,
-		ip_address TEXT NOT NULL,
 		username TEXT NOT NULL UNIQUE,
+		hashed_password TEXT NOT NULL,
 		timezone TEXT DEFAULT 'America/New_York',
 		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 	)`
 	db.db.Exec(stmt)
 }
 
-func (db *Database) UserTimezoneSet(ip_address, timezone string) {
-	_, err := db.db.Exec("UPDATE users SET timezone = ? WHERE ip_address = ?", timezone, ip_address)
+func (db *Database) UserTimezoneSet(username, timezone string) {
+	_, err := db.db.Exec("UPDATE users SET timezone = ? WHERE username = ?", timezone, username)
 	if err != nil {
 		fmt.Println(err)
 	}
 }
 
-func (db *Database) UserAdd(ip_address, username string) {
-	_, err := db.db.Exec("INSERT INTO users (username, ip_address) VALUES (?, ?)", username, ip_address)
-	if err != nil {
-		fmt.Println(err)
+func (db *Database) UserAddWithPassword(username, unhashedPwd string) error {
+	// unhashedPwd can not be larger than 72 bytes
+	if len(unhashedPwd) > 72 {
+		return fmt.Errorf("Password too long")
 	}
+	hashedPwd, err := bcrypt.GenerateFromPassword([]byte(unhashedPwd), bcrypt.DefaultCost)
+	if err != nil {
+		return err
+	}
+	_, err = db.db.Exec("INSERT INTO users (username, hashed_password) VALUES (?, ?)", username, hashedPwd)
+	if err != nil {
+		return err
+	}
+	return nil
 }
 
-func (db *Database) MessageAdd(ip_address string, content string) {
-	_, err := db.db.Exec("INSERT INTO messages (ip_address, content) VALUES (?, ?)", ip_address, content)
-	if err != nil {
-		fmt.Println(err)
-	}
-}
-
-func (db *Database) UserNameGet(ip_address string) string {
-	rows, err := db.db.Query("SELECT username FROM users WHERE ip_address = ?", ip_address)
+func (db *Database) UserPasswordCheck(username, unhashedPwd string) bool {
+	rows, err := db.db.Query("SELECT hashed_password FROM users WHERE username = ?", username)
 	if err != nil {
 		fmt.Println(err)
 	}
 	defer rows.Close()
-	var username string
+	var hashedPwd string
 	rows.Next()
-	rows.Scan(&username)
-	return username
+	rows.Scan(&hashedPwd)
+	err = bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(unhashedPwd))
+	return err == nil
 }
 
-func (db *Database) UserIpGet(username string) string {
-	rows, err := db.db.Query("SELECT ip_address FROM users WHERE username = ?", username)
+func (db *Database) MessageAdd(username string, content string) {
+	_, err := db.db.Exec("INSERT INTO messages (username, content) VALUES (?, ?)", username, content)
 	if err != nil {
 		fmt.Println(err)
 	}
-	defer rows.Close()
-	var ip_address string
-	rows.Next()
-	rows.Scan(&ip_address)
-	return ip_address
 }
 
-func (db *Database) UserGetTimezone(ip_address string) string {
-	rows, err := db.db.Query("SELECT timezone FROM users WHERE ip_address = ?", ip_address)
+func (db *Database) UserGetTimezone(username string) string {
+	rows, err := db.db.Query("SELECT timezone FROM users WHERE username = ?", username)
 	if err != nil {
 		fmt.Println(err)
 	}
@@ -130,16 +128,14 @@ func (db *Database) UsersGet() []User {
 	var users []User
 	for rows.Next() {
 		var id string
-		var ip_address string
 		var username string
 		var created_at string
 		var timezone string
-		rows.Scan(&id, &ip_address, &username, &created_at, &timezone)
+		rows.Scan(&id, &username, &created_at, &timezone)
 		user := User{
-			Id:        id,
-			Username:  username,
-			IpAddress: ip_address,
-			Timezone:  timezone,
+			Id:       id,
+			Username: username,
+			Timezone: timezone,
 		}
 		users = append(users, user)
 	}
@@ -148,11 +144,14 @@ func (db *Database) UsersGet() []User {
 
 func (db *Database) MessagesGet() []Message {
 	rows, err := db.db.Query(`
-		SELECT messages.id, messages.ip_address, messages.content, 
-		strftime('%Y-%m-%d %H:%M:%S', messages.created_at) as created_at, 
-		users.username, messages.edited
-		FROM messages 
-		LEFT JOIN users ON messages.ip_address = users.ip_address;
+		SELECT 
+			messages.id,
+			messages.username,
+			messages.content,
+			strftime('%Y-%m-%d %H:%M:%S', messages.created_at) as created_at,
+			messages.edited
+		FROM
+			messages 
 	`)
 	if err != nil {
 		fmt.Println(err)
@@ -164,11 +163,10 @@ func (db *Database) MessagesGet() []Message {
 	for rows.Next() {
 		var id string
 		var content string
-		var ip_address string
 		var created_at string
 		var username string
 		var edited int
-		rows.Scan(&id, &ip_address, &content, &created_at, &username, &edited)
+		rows.Scan(&id, &username, &content, &created_at, &edited)
 
 		editedBool := false
 		if edited == 1 {
@@ -178,7 +176,6 @@ func (db *Database) MessagesGet() []Message {
 		message := Message{
 			Id:             id,
 			Content:        content,
-			SenderIp:       ip_address,
 			SenderUsername: username,
 			Edited:         editedBool,
 			Timestamp:      created_at,
@@ -198,8 +195,8 @@ func (db *Database) UserNameExists(username string) bool {
 	return rows.Next()
 }
 
-func (db *Database) UserExists(ip string) bool {
-	rows, err := db.db.Query("SELECT * FROM users WHERE ip_address = ?", ip)
+func (db *Database) UserExists(username string) bool {
+	rows, err := db.db.Query("SELECT * FROM users WHERE username = ?", username)
 	if err != nil {
 		fmt.Println(err)
 	}
@@ -207,28 +204,21 @@ func (db *Database) UserExists(ip string) bool {
 	return rows.Next()
 }
 
-func (db *Database) UserNameChange(ip, newUsername string) {
-	_, err := db.db.Exec("UPDATE users SET username = ? WHERE ip_address = ?", newUsername, ip)
+func (db *Database) UserNameChange(oldUsername, newUsername string) {
+	_, err := db.db.Exec("UPDATE users SET username = ? WHERE username = ?", newUsername, oldUsername)
 	if err != nil {
 		fmt.Println(err)
 	}
 }
 
-func (db *Database) UserMessagesDelete(ip string) {
-	_, err := db.db.Exec("DELETE FROM messages WHERE ip_address = ?", ip)
-	if err != nil {
-		fmt.Println(err)
-	}
-}
-
-func (db *Database) UserMessagesGet(ip string) []Message {
+func (db *Database) UserMessagesGet(username string) []Message {
 	rows, err := db.db.Query(`
 		SELECT messages.*, users.username 
 		FROM messages 
-		LEFT JOIN users ON messages.ip_address = users.ip_address 
-		WHERE messages.ip_address = ?
+		LEFT JOIN users ON messages.username = users.username 
+		WHERE messages.username = ?
 		ORDER BY messages.created_at DESC;
-	`, ip)
+	`, username)
 	if err != nil {
 		fmt.Println(err)
 	}
@@ -239,11 +229,10 @@ func (db *Database) UserMessagesGet(ip string) []Message {
 	for rows.Next() {
 		var id string
 		var content string
-		var ip_address string
 		var created_at string
 		var username string
 		var edited int
-		rows.Scan(&id, &ip_address, &content, &created_at, &username, &edited)
+		rows.Scan(&id, &content, &created_at, &username, &edited)
 		t, _ := time.Parse(created_at, created_at)
 		editedBool := false
 		if edited == 1 {
@@ -252,7 +241,6 @@ func (db *Database) UserMessagesGet(ip string) []Message {
 		message := Message{
 			Id:             id,
 			Content:        content,
-			SenderIp:       ip_address,
 			SenderUsername: username,
 			Edited:         editedBool,
 			Timestamp:      t.Format(created_at),
@@ -268,8 +256,8 @@ func (db *Database) MessageDeleteId(id string) {
 		fmt.Println(err)
 	}
 }
-func (db *Database) MessageDeleteIfOwner(id string, ip string) (int, error) {
-	res, err := db.db.Exec("DELETE FROM messages WHERE id = ? AND ip_address = ?", id, ip)
+func (db *Database) MessageDeleteIfOwner(id string, username string) (int, error) {
+	res, err := db.db.Exec("DELETE FROM messages WHERE id = ? AND username = ?", id, username)
 	if err != nil {
 		return 0, err
 	}
@@ -281,8 +269,8 @@ func (db *Database) MessageDeleteIfOwner(id string, ip string) (int, error) {
 
 }
 
-func (db *Database) MessageEditIfOwner(id string, content string, ip string) (int, error) {
-	res, err := db.db.Exec("UPDATE messages SET content = ?, edited = 1 WHERE id = ? AND ip_address = ?", content, id, ip)
+func (db *Database) MessageEditIfOwner(id string, content string, username string) (int, error) {
+	res, err := db.db.Exec("UPDATE messages SET content = ?, edited = 1 WHERE id = ? AND username = ?", content, id, username)
 	if err != nil {
 		return 0, err
 	}
@@ -304,8 +292,8 @@ func (db *Database) DeleteOldMessages(ageMinutes int) {
 	}
 }
 
-func (db *Database) UserDeleteIp(ip string) {
-	_, err := db.db.Exec("DELETE FROM users WHERE ip_address = ?", ip)
+func (db *Database) UserDelete(username string) {
+	_, err := db.db.Exec("DELETE FROM users WHERE username = ?", username)
 	if err != nil {
 		fmt.Println(err)
 	}
diff --git a/go.mod b/go.mod
index d84ee9d..073c576 100644
--- a/go.mod
+++ b/go.mod
@@ -3,3 +3,5 @@ module chat
 go 1.23.4
 
 require github.com/mattn/go-sqlite3 v1.14.24 // direct
+
+require golang.org/x/crypto v0.32.0 // indirect
diff --git a/go.sum b/go.sum
index 9dcdc9b..b996447 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,4 @@
 github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
 github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
diff --git a/public/index.js b/public/index.js
index 3f9fdaf..bec0ae4 100644
--- a/public/index.js
+++ b/public/index.js
@@ -433,19 +433,33 @@ async function checkUsername() {
 		const response = await fetch("/username/status");
 		const data = await response.json();
 		if (!data.hasUsername) {
-			document.getElementById("settings-panel").style
-				.display = "block";
-			const username = document.getElementById("username");
-			username.focus();
-			username.selectionStart =
-				username.selectionEnd =
-				username.value.length;
+			// redirect to login page
+			window.location.href = "/login";
+			//
+			//
+			// document.getElementById("settings-panel").style
+			// 	.display = "block";
+			// const username = document.getElementById("username");
+			// username.focus();
+			// username.selectionStart =
+			// 	username.selectionEnd =
+			// 	username.value.length;
 		}
 	} catch (error) {
 		console.error("Error checking username status:", error);
 	}
 }
 
+async function getCurrentUsername() {
+	try {
+		const response = await fetch("/username/status");
+		const data = await response.json();
+		return data.username;
+	} catch (error) {
+		console.error("Error getting username:", error);
+	}
+}
+
 async function updateCurrentUser() {
 	try {
 		const response = await fetch("/username/status");
@@ -508,15 +522,15 @@ async function sendMessage() {
 	if (!message) {
 		return;
 	}
-
 	try {
 		lastMessage = message;
+		const username = await getCurrentUsername();
 		const response = await fetch("/messages", {
 			method: "PUT",
 			headers: {
 				"Content-Type": "application/json",
 			},
-			body: JSON.stringify({ message: message }),
+			body: JSON.stringify({ username: username, message: message }),
 		});
 
 		const data = await response.json();
diff --git a/public/login.html b/public/login.html
new file mode 100644
index 0000000..b22fdc6
--- /dev/null
+++ b/public/login.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="X-UA-Compatible" content="ie=edge">
+        <meta http-equiv="Cache-Control" content="max-age=86400, must-revalidate">
+        <meta http-equiv="Pragma" content="cache">
+        <meta http-equiv="Expires" content="86400">
+        <style>
+            :root{
+                    --radchat-color: #40a02b;
+                    --main-bg-color: #11111b;
+                    --pum-button-inactive-fg: #cdd6f4;
+                    --pum-button-inactive-bg: #11111b;
+                    --pum-button-active-fg: #89b4fa;
+                    --pum-button-active-bg: #1e1e2e;
+                    --pum-title-color: #cdd6f4;
+                    --pum-bg-color: #1e1e2e;
+                    --user-color: #89b4fa;
+                    --timestamp-color: #313244;
+                    --separator-color: #181825;
+                    --message-color: #cdd6f4;
+                    --message-bg-color: #1e1e2e;
+                    --input-bg-color: #181825;
+                    --input-text-color: #cdd6f4;
+                    --input-button-inactive-bg: #b4befe;
+                    --input-button-inactive-fg: #11111b;
+                    --input-button-active-bg: #89b4fa;
+                    --input-button-active-fg: #11111b;
+            }
+
+            body {
+                margin: 0;
+                padding: 0;
+                min-height: 100vh;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                background-color: var(--main-bg-color);
+                font-family: system-ui, -apple-system, sans-serif;
+            }
+
+            .login-container, .signup-container {
+                background-color: var(--pum-bg-color);
+                border-radius: 12px;
+                padding: 2rem;
+                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+                width: 100%;
+                max-width: 400px;
+                margin: 1rem;
+            }
+
+            .login-form, .signup-form {
+                display: flex;
+                flex-direction: column;
+                gap: 1.5rem;
+            }
+
+            h1 {
+                color: var(--radchat-color);
+                text-align: center;
+                margin: 0 0 1.5rem 0;
+                font-size: 2.5rem;
+            }
+
+            .login-input, .signup-input {
+                padding: 0.75rem;
+                border: none;
+                border-radius: 6px;
+                background-color: var(--input-bg-color);
+                color: var(--input-text-color);
+                font-size: 1rem;
+                width: 100%;
+                box-sizing: border-box;
+            }
+
+            .login-button, .signup-button {
+                padding: 0.75rem;
+                border: none;
+                border-radius: 6px;
+                background-color: var(--input-button-inactive-bg);
+                color: var(--input-button-inactive-fg);
+                font-size: 1rem;
+                cursor: pointer;
+                transition: background-color 0.2s;
+            }
+
+            .login-button:hover, .signup-button:hover {
+                background-color: var(--input-button-active-bg);
+                color: var(--input-button-active-fg);
+            }
+
+            .login-error, .signup-error {
+                color: #e64553;
+                text-align: center;
+                min-height: 1.5rem;
+            }
+
+            .signup-link {
+                text-align: center;
+                color: var(--message-color);
+            }
+
+            .signup-link a {
+                color: var(--user-color);
+                text-decoration: none;
+            }
+
+            .signup-link a:hover {
+                text-decoration: underline;
+            }
+        </style>
+        <title>RadChat Login</title>
+        <script>
+            function login() {
+                const loginUsername = document.getElementById('loginUsername').value;
+                const loginPassword = document.getElementById('loginPassword').value;
+                const loginError = document.getElementById('loginError');
+                fetch('/login', {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                    },
+                    body: JSON.stringify({
+                        username: loginUsername,
+                        password: loginPassword
+                    }),
+                })
+                    .then((response) => response.json())
+                    .then((data) => {
+                        if (data.error) {
+                            loginError.innerHTML = data.error;
+                        } else {
+                            window.location.href = '/';
+                        }
+                    });
+                return false;
+            }
+        </script>
+        <link rel="stylesheet" href="root.css">
+
+    </head>
+    <body>
+        <!-- Login Form -->
+        <div class="login-container">
+            <div class="login-form">
+                <h1>RadChat</h1>
+                <form id="loginForm" onsubmit="return login()">
+                    <input
+                        type="text"
+                        id="loginUsername"
+                        class="login-input"
+                        placeholder="Username"
+                        required
+                    >
+                    <input
+                        type="password"
+                        id="loginPassword"
+                        class="login-input"
+                        placeholder="Password"
+                        required
+                    >
+                    <button
+                        type="submit"
+                        class="login-button"
+                    >
+                        Login
+                    </button>
+                    <div id="loginError" class="login-error"></div>
+                </form>
+                <a href="/signup" class="signup-link">Signup</a>
+            </div>
+        </div>
+    </body>
+</html>
+
+
diff --git a/public/signup.html b/public/signup.html
new file mode 100644
index 0000000..672fe2f
--- /dev/null
+++ b/public/signup.html
@@ -0,0 +1,186 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="X-UA-Compatible" content="ie=edge">
+        <meta http-equiv="Cache-Control" content="max-age=86400, must-revalidate">
+        <meta http-equiv="Pragma" content="cache">
+        <meta http-equiv="Expires" content="86400">
+        <style>
+            :root{
+                    --radchat-color: #40a02b;
+                    --main-bg-color: #11111b;
+                    --pum-button-inactive-fg: #cdd6f4;
+                    --pum-button-inactive-bg: #11111b;
+                    --pum-button-active-fg: #89b4fa;
+                    --pum-button-active-bg: #1e1e2e;
+                    --pum-title-color: #cdd6f4;
+                    --pum-bg-color: #1e1e2e;
+                    --user-color: #89b4fa;
+                    --timestamp-color: #313244;
+                    --separator-color: #181825;
+                    --message-color: #cdd6f4;
+                    --message-bg-color: #1e1e2e;
+                    --input-bg-color: #181825;
+                    --input-text-color: #cdd6f4;
+                    --input-button-inactive-bg: #b4befe;
+                    --input-button-inactive-fg: #11111b;
+                    --input-button-active-bg: #89b4fa;
+                    --input-button-active-fg: #11111b;
+            }
+
+            body {
+                margin: 0;
+                padding: 0;
+                min-height: 100vh;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                background-color: var(--main-bg-color);
+                font-family: system-ui, -apple-system, sans-serif;
+            }
+
+            .login-container, .signup-container {
+                background-color: var(--pum-bg-color);
+                border-radius: 12px;
+                padding: 2rem;
+                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+                width: 100%;
+                max-width: 400px;
+                margin: 1rem;
+            }
+
+            .login-form, .signup-form {
+                display: flex;
+                flex-direction: column;
+                gap: 1.5rem;
+            }
+
+            h1 {
+                color: var(--radchat-color);
+                text-align: center;
+                margin: 0 0 1.5rem 0;
+                font-size: 2.5rem;
+            }
+
+            .login-input, .signup-input {
+                padding: 0.75rem;
+                border: none;
+                border-radius: 6px;
+                background-color: var(--input-bg-color);
+                color: var(--input-text-color);
+                font-size: 1rem;
+                width: 100%;
+                box-sizing: border-box;
+            }
+
+            .login-button, .signup-button {
+                padding: 0.75rem;
+                border: none;
+                border-radius: 6px;
+                background-color: var(--input-button-inactive-bg);
+                color: var(--input-button-inactive-fg);
+                font-size: 1rem;
+                cursor: pointer;
+                transition: background-color 0.2s;
+            }
+
+            .login-button:hover, .signup-button:hover {
+                background-color: var(--input-button-active-bg);
+                color: var(--input-button-active-fg);
+            }
+
+            .login-error, .signup-error {
+                color: #e64553;
+                text-align: center;
+                min-height: 1.5rem;
+            }
+
+            .signup-link {
+                text-align: center;
+                color: var(--message-color);
+            }
+
+            .signup-link a {
+                color: var(--user-color);
+                text-decoration: none;
+            }
+
+            .signup-link a:hover {
+                text-decoration: underline;
+            }
+        </style>
+        <title>RadChat Signup</title>
+        <script>
+            function signup() {
+                const signupUsername = document.getElementById('signupUsername').value;
+                const signupPassword = document.getElementById('signupPassword').value;
+                const signupConfirmPassword = document.getElementById('signupConfirmPassword').value;
+                const signupError = document.getElementById('signupError');
+                if (signupPassword !== signupConfirmPassword) {
+                    signupError.innerHTML = 'Passwords do not match';
+                    return false;
+                }
+                fetch('/signup', {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                    },
+                    body: JSON.stringify({
+                        username: signupUsername,
+                        password: signupPassword,
+                    }),
+                })
+                    .then((response) => response.json())
+                    .then((data) => {
+                        if (data.error) {
+                            signupError.innerHTML = data.error;
+                        } else {
+                            window.location.href = '/login';
+                        }
+                    });
+                return false;
+            }
+        </script>
+        <link rel="stylesheet" href="root.css">
+    </head>
+    <body>
+        <!-- Signup Form -->
+        <div class="signup-container">
+            <div class="signup-form">
+                <h1>RadChat</h1>
+                <form id="signupForm" onsubmit="return signup()">
+                    <input
+                        type="text"
+                        id="signupUsername"
+                        class="signup-input"
+                        placeholder="Username"
+                        required
+                    >
+                    <input
+                        type="password"
+                        id="signupPassword"
+                        class="signup-input"
+                        placeholder="Password"
+                        required
+                    >
+                    <input
+                        type="password"
+                        id="signupConfirmPassword"
+                        class="signup-input"
+                        placeholder="Confirm Password"
+                        required
+                    >
+                    <button type="submit" class="signup-button">Sign Up</button>
+                </form>
+                <div class="signup-error" id="signupError"></div>
+                <div class="signup-link">
+                    Already have an account? <a href="/login">Log In</a>
+                </div>
+            </div>
+
+    </body>
+</html>
+
+
diff --git a/readme.md b/readme.md
index 44ba47e..ee23d8f 100644
--- a/readme.md
+++ b/readme.md
@@ -14,6 +14,13 @@
 - Lazy load with pagination (frontend and backend)
 - Add live voice chat? (This will be fun, maybe a separate app)
 ### Mid Priority
-- Add actual logging instead of ip based usernames, have messages tied to the logged in user not an ip (db changes)
+- NEW LOGIN STUFF
+    - IN PROGRESS: Add actual logging instead of ip based usernames, have messages tied to the logged in user not an ip (db changes)
+    * Fix editing messages
+    * Fix deleting messages
+    * Fix changing username
+    * Fix CSS for signin page
+    * Add logout button to settings, should touch go, js all that, logout request
+    * Fix CSS for login page
 ### Low Priority
 - Nothing yet 
diff --git a/srv/handle.go b/srv/handle.go
index 94daa9e..b912589 100644
--- a/srv/handle.go
+++ b/srv/handle.go
@@ -4,9 +4,21 @@ import (
 	tu "chat/tu"
 	"encoding/json"
 	"fmt"
+	"math/rand"
 	"net/http"
 )
 
+func generateName() string {
+	adjectives := []string{"Unrelenting", "Mystical", "Radiant", "Curious", "Peaceful", "Ancient", "Wandering", "Silent", "Celestial", "Dancing", "Eternal", "Resolute", "Whispering", "Serene", "Wild"}
+	colors := []string{"Purple", "Azure", "Crimson", "Golden", "Emerald", "Sapphire", "Obsidian", "Silver", "Amber", "Jade", "Indigo", "Violet", "Cerulean", "Copper", "Pearl"}
+	nouns := []string{"Elephant", "Phoenix", "Dragon", "Warrior", "Spirit", "Tiger", "Raven", "Mountain", "River", "Storm", "Falcon", "Wolf", "Ocean", "Star", "Moon"}
+
+	return fmt.Sprintf("%s-%s-%s",
+		adjectives[rand.Intn(len(adjectives))],
+		colors[rand.Intn(len(colors))],
+		nouns[rand.Intn(len(nouns))])
+}
+
 func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
 	clientIP := getClientIP(r)
 	s.updateActivity(clientIP)
@@ -33,35 +45,32 @@ func (s *Server) handleUsername(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	s.mu.Lock()
-
 	if len(req.Username) > s.Config.Options.NameMaxLength {
 		http.Error(w, fmt.Sprintf(`{"error": "Username too long (%v out of %v characters maximum)"}`, len(req.Username), s.Config.Options.NameMaxLength), http.StatusRequestEntityTooLarge)
-		s.mu.Unlock()
 		return
 	}
 
 	if !validUsername(req.Username) {
 		http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
-		s.mu.Unlock()
 		return
 	}
 
 	if s.Database.UserNameExists(req.Username) {
 		http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
-		s.mu.Unlock()
 		return
 	}
 
-	if s.Database.UserExists(clientIP) {
-		s.Database.UserNameChange(clientIP, req.Username)
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if username, ok := s.LoggedIn[clientIP]; ok {
+		s.LogUserOut(username)
+		s.Database.UserNameChange(username, req.Username)
+		s.LogUserIn(clientIP, req.Username)
+		json.NewEncoder(w).Encode(map[string]string{"status": "Username changed"})
 	} else {
-		s.Database.UserAdd(clientIP, req.Username)
+		http.Error(w, `{"error": "Failure to change username"}`, http.StatusUnauthorized)
 	}
 
-	s.mu.Unlock()
-
-	json.NewEncoder(w).Encode(map[string]string{"status": "Username registered"})
 }
 
 func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
@@ -97,7 +106,11 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
 		messages := s.Database.MessagesGet()
 		for _, msg := range messages {
 			clientIP := getClientIP(r)
-			timeZone := s.Database.UserGetTimezone(clientIP)
+			username, ok := s.LoggedIn[clientIP]
+			timeZone := "UTC"
+			if ok {
+				timeZone = s.Database.UserGetTimezone(username)
+			}
 			timeLocal := tu.TimeStringToTimeInLocation(msg.Timestamp, timeZone)
 			edited := ""
 			if msg.Edited {
@@ -111,22 +124,9 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
 	case http.MethodPut:
 		w.Header().Set("Content-Type", "application/json")
 
-		// Get client's IP
-		clientIP := getClientIP(r)
-
-		s.mu.Lock()
-		exists := s.Database.UserExists(clientIP)
-		username := s.Database.UserNameGet(clientIP)
-		s.mu.Unlock()
-
-		if !exists {
-			errorFmt := fmt.Sprintf(`{"error": "IP %s not registered with username"}`, clientIP)
-			http.Error(w, errorFmt, http.StatusUnauthorized)
-			return
-		}
-
 		var msg struct {
-			Message string `json:"message"`
+			Username string `json:"username"`
+			Message  string `json:"message"`
 		}
 
 		if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
@@ -134,12 +134,19 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		s.Database.MessageAdd(clientIP, msg.Message)
+		clientIP := getClientIP(r)
+		if username, ok := s.LoggedIn[clientIP]; ok {
+			s.mu.Lock()
+			defer s.mu.Unlock()
+			s.Database.MessageAdd(username, msg.Message)
+			json.NewEncoder(w).Encode(map[string]string{
+				"status": "Message received",
+				"from":   username,
+			})
+		} else {
+			http.Error(w, `{"error": "Unauthorized"}`, http.StatusUnauthorized)
+		}
 
-		json.NewEncoder(w).Encode(map[string]string{
-			"status": "Message received",
-			"from":   username,
-		})
 	case http.MethodDelete:
 		w.Header().Set("Content-Type", "application/json")
 
@@ -171,21 +178,16 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
 }
 
 func (s *Server) handleUsernameStatus(w http.ResponseWriter, r *http.Request) {
-	if r.Method != http.MethodGet {
-		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
-		return
-	}
+	// if r.Method != http.MethodGet {
+	// 	http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
+	// 	return
+	// }
+	clientIP := getClientIP(r)
+	username, ok := s.LoggedIn[clientIP]
 
 	w.Header().Set("Content-Type", "application/json")
-	clientIP := getClientIP(r)
-
-	s.mu.Lock()
-	exists := s.Database.UserExists(clientIP)
-	username := s.Database.UserNameGet(clientIP)
-	s.mu.Unlock()
-
 	json.NewEncoder(w).Encode(map[string]interface{}{
-		"hasUsername": exists,
+		"hasUsername": ok,
 		"username":    username,
 	})
 }
@@ -202,12 +204,14 @@ func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
 	s.updateActivity(clientIP)
 	s.cleanupActivity()
 	s.mu.Lock()
+	defer s.mu.Unlock()
 	var users []string
 	for ip := range s.Connected {
 		// for all connected, get their usernames
-		users = append(users, s.Database.UserNameGet(ip))
+		if username, ok := s.LoggedIn[ip]; ok {
+			users = append(users, username)
+		}
 	}
-	s.mu.Unlock()
 
 	json.NewEncoder(w).Encode(map[string]interface{}{
 		"users": users,
@@ -233,18 +237,15 @@ func (s *Server) handleTimezone(w http.ResponseWriter, r *http.Request) {
 	}
 
 	s.mu.Lock()
-
-	if !s.Database.UserExists(clientIP) {
+	defer s.mu.Unlock()
+	if username, ok := s.LoggedIn[clientIP]; ok {
+		s.Database.UserTimezoneSet(username, req.Timezone)
+		json.NewEncoder(w).Encode(map[string]string{
+			"status": "success",
+		})
+	} else {
 		http.Error(w, `{"error": "User not registered"}`, http.StatusUnauthorized)
-		s.mu.Unlock()
-		return
 	}
-
-	s.Database.UserTimezoneSet(clientIP, req.Timezone)
-	s.mu.Unlock()
-	json.NewEncoder(w).Encode(map[string]string{
-		"status": "success",
-	})
 }
 
 func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
@@ -283,3 +284,90 @@ func (s *Server) handleMessagesLength(w http.ResponseWriter, r *http.Request) {
 		"length": len(messages),
 	})
 }
+
+func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case http.MethodGet:
+		w.Header().Set("Content-Type", "text/html")
+		file := readFile(s.Config.Paths.LoginHtmlPath)
+		w.Write(file)
+		return
+	case http.MethodPost:
+		w.Header().Set("Content-Type", "application/json")
+		var req struct {
+			Username string `json:"username"`
+			Password string `json:"password"`
+		}
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+			http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
+			return
+		}
+
+		validLogin := s.Database.UserPasswordCheck(req.Username, req.Password)
+		if !validLogin {
+			http.Error(w, `{"error": "Invalid username or password"}`, http.StatusUnauthorized)
+			return
+		} else {
+			clientIP := getClientIP(r)
+			s.LogUserIn(clientIP, req.Username)
+			json.NewEncoder(w).Encode(map[string]string{
+				"status": "Logged in",
+			})
+		}
+	}
+}
+
+func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case http.MethodGet:
+		w.Header().Set("Content-Type", "text/html")
+		file := readFile(s.Config.Paths.SignupHtmlPath)
+		w.Write(file)
+		return
+	case http.MethodPost:
+		w.Header().Set("Content-Type", "application/json")
+		var req struct {
+			Username string `json:"username"`
+			Password string `json:"password"`
+		}
+
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+			http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
+			return
+		}
+
+		// validate username length
+		if len(req.Username) > s.Config.Options.NameMaxLength {
+			http.Error(w, fmt.Sprintf(`{"error": "Username too long (%v out of %v characters maximum)"}`, len(req.Username), s.Config.Options.NameMaxLength), http.StatusRequestEntityTooLarge)
+			return
+		}
+
+		// validate username
+		if !validUsername(req.Username) {
+			http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
+			return
+		}
+
+		// validate user doesnt already exist
+		if s.Database.UserNameExists(req.Username) {
+			http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
+			return
+		}
+
+		// add user to database with hashedpassword
+		err := s.Database.UserAddWithPassword(req.Username, req.Password)
+		if err != nil {
+			fmt.Println("Database error while signing up a new user")
+			http.Error(w, fmt.Sprintf(`{"error": "%v"}`, err), http.StatusInternalServerError)
+			return
+		}
+
+		// log user in
+		clientIP := getClientIP(r)
+		s.LogUserIn(clientIP, req.Username)
+
+		json.NewEncoder(w).Encode(map[string]string{
+			"status": "User created",
+		})
+	}
+}
diff --git a/srv/srv.go b/srv/srv.go
index 6226f98..437fd51 100644
--- a/srv/srv.go
+++ b/srv/srv.go
@@ -21,15 +21,17 @@ type Config struct {
 		Port      int    `json:"port"`
 	} `json:"server"`
 	Paths struct {
-		DatabasePath     string `json:"databasePath"`
-		IndexJsPath      string `json:"indexJsPath"`
-		IndexCssPath     string `json:"indexCssPath"`
-		IndexHtmlPath    string `json:"indexHtmlPath"`
-		MessagesHtmlPath string `json:"messagesHtmlPath"`
+		DatabasePath   string `json:"databasePath"`
+		IndexJsPath    string `json:"indexJsPath"`
+		IndexCssPath   string `json:"indexCssPath"`
+		IndexHtmlPath  string `json:"indexHtmlPath"`
+		SignupHtmlPath string `json:"signupHtmlPath"`
+		LoginHtmlPath  string `json:"loginHtmlPath"`
 	} `json:"paths"`
 	Options struct {
-		MessageMaxAge int `json:"messageMaxAge"`
-		NameMaxLength int `json:"nameMaxLength"`
+		MessageMaxAge  int `json:"messageMaxAge"`
+		NameMaxLength  int `json:"nameMaxLength"`
+		MessagePerPage int `json:"messagePerPage"`
 	} `json:"options"`
 }
 
@@ -40,7 +42,8 @@ func LoadConfig(filepath string) Config {
 	config.Paths.IndexHtmlPath = pathMaker(config.Paths.IndexHtmlPath)
 	config.Paths.IndexJsPath = pathMaker(config.Paths.IndexJsPath)
 	config.Paths.IndexCssPath = pathMaker(config.Paths.IndexCssPath)
-	config.Paths.MessagesHtmlPath = pathMaker(config.Paths.MessagesHtmlPath)
+	config.Paths.SignupHtmlPath = pathMaker(config.Paths.SignupHtmlPath)
+	config.Paths.LoginHtmlPath = pathMaker(config.Paths.LoginHtmlPath)
 	config.Paths.DatabasePath = pathMaker(config.Paths.DatabasePath)
 	if err != nil {
 		fmt.Println("Error parsing config file: ", err)
@@ -118,6 +121,7 @@ func validUsername(username string) bool {
 }
 
 type Server struct {
+	LoggedIn  map[string]string    // Map Username -> IP
 	Connected map[string]time.Time // Map IP -> Last activity time
 	Database  *db.Database
 	Config    Config
@@ -127,6 +131,7 @@ type Server struct {
 func NewServer(config Config) *Server {
 
 	return &Server{
+		LoggedIn:  make(map[string]string),
 		Connected: make(map[string]time.Time),
 		Database:  db.OpenDatabase(config.Paths.DatabasePath),
 		Config:    config,
@@ -138,6 +143,23 @@ func (s *Server) AddMessage(userip string, contents string) {
 	s.Database.MessageAdd(userip, contents)
 }
 
+func (s *Server) LogUserIn(ip, username string) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.LoggedIn[ip] = username
+}
+
+func (s *Server) LogUserOut(username string) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	for ip, u := range s.LoggedIn {
+		if u == username {
+			delete(s.LoggedIn, ip)
+		}
+	}
+	delete(s.LoggedIn, username)
+}
+
 func (s *Server) updateActivity(ip string) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
@@ -150,6 +172,7 @@ func (s *Server) cleanupActivity() {
 	for ip, lastActivity := range s.Connected {
 		if time.Since(lastActivity) > 10*time.Second {
 			delete(s.Connected, ip)
+			s.LogUserOut(s.LoggedIn[ip])
 		}
 	}
 }
@@ -169,6 +192,8 @@ func (s *Server) Run() {
 	handler.HandleFunc("/users", s.handleUsers)
 	handler.HandleFunc("/username", s.handleUsername)
 	handler.HandleFunc("/messages", s.handleMessages)
+	handler.HandleFunc("/signup", s.handleSignup)
+	handler.HandleFunc("/login", s.handleLogin)
 	fmt.Printf("Server starting on %s:%d\n", s.Config.Server.IpAddress, s.Config.Server.Port)
 	defer s.Stop()
 	if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Config.Server.IpAddress, s.Config.Server.Port), GzipMiddleware(handler)); err != nil {