Compare commits

..

1 Commits

Author SHA1 Message Date
5f43cde2d6 readme 2025-01-23 20:04:57 -06:00
10 changed files with 149 additions and 641 deletions

View File

@ -4,16 +4,13 @@
"port": 8080 "port": 8080
}, },
"paths": { "paths": {
"databasePath": "/home/radon/Documents/chat.db", "databasePath": "/home/radon/Documents/chattest.db",
"indexJsPath": "./public/index.js", "indexJsPath": "./public/index.js",
"indexCssPath": "./public/style.css", "indexCssPath": "./public/style.css",
"indexHtmlPath": "./public/index.html", "indexHtmlPath": "./public/index.html"
"signupHtmlPath": "./public/signup.html",
"loginHtmlPath": "./public/login.html"
}, },
"options": { "options": {
"messageMaxAge": 259200, "messageMaxAge": 259200,
"nameMaxLength": 32, "nameMaxLength": 32
"messagesPerPage": 10
} }
} }

130
db/db.go
View File

@ -4,20 +4,20 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
"strconv" "strconv"
"time" "time"
) )
type User struct { type User struct {
Id string Id string
Username string Username string
Timezone string IpAddress string
HashedPassword string Timezone string
} }
type Message struct { type Message struct {
Id string Id string
SenderIp string
SenderUsername string SenderUsername string
Content string Content string
Timestamp string Timestamp string
@ -45,7 +45,7 @@ func (db *Database) Close() {
func (db *Database) DbCreateTableMessages() { func (db *Database) DbCreateTableMessages() {
stmt := `CREATE TABLE IF NOT EXISTS messages ( stmt := `CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL, ip_address TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
edited INTEGER DEFAULT 0, edited INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@ -56,59 +56,61 @@ func (db *Database) DbCreateTableMessages() {
func (db *Database) DbCreateTableUsers() { func (db *Database) DbCreateTableUsers() {
stmt := `CREATE TABLE IF NOT EXISTS users ( stmt := `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
hashed_password TEXT NOT NULL,
timezone TEXT DEFAULT 'America/New_York', timezone TEXT DEFAULT 'America/New_York',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)` )`
db.db.Exec(stmt) db.db.Exec(stmt)
} }
func (db *Database) UserTimezoneSet(username, timezone string) { func (db *Database) UserTimezoneSet(ip_address, timezone string) {
_, err := db.db.Exec("UPDATE users SET timezone = ? WHERE username = ?", timezone, username) _, err := db.db.Exec("UPDATE users SET timezone = ? WHERE ip_address = ?", timezone, ip_address)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
} }
func (db *Database) UserAddWithPassword(username, unhashedPwd string) error { func (db *Database) UserAdd(ip_address, username string) {
// unhashedPwd can not be larger than 72 bytes _, err := db.db.Exec("INSERT INTO users (username, ip_address) VALUES (?, ?)", username, ip_address)
if len(unhashedPwd) > 72 {
return fmt.Errorf("Password too long")
}
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(unhashedPwd), bcrypt.DefaultCost)
if err != nil { if err != nil {
return err fmt.Println(err)
} }
_, err = db.db.Exec("INSERT INTO users (username, hashed_password) VALUES (?, ?)", username, hashedPwd)
if err != nil {
return err
}
return nil
} }
func (db *Database) UserPasswordCheck(username, unhashedPwd string) bool { func (db *Database) MessageAdd(ip_address string, content string) {
rows, err := db.db.Query("SELECT hashed_password FROM users WHERE username = ?", username) _, 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)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
defer rows.Close() defer rows.Close()
var hashedPwd string var username string
rows.Next() rows.Next()
rows.Scan(&hashedPwd) rows.Scan(&username)
err = bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(unhashedPwd)) return username
return err == nil
} }
func (db *Database) MessageAdd(username string, content string) { func (db *Database) UserIpGet(username string) string {
_, err := db.db.Exec("INSERT INTO messages (username, content) VALUES (?, ?)", username, content) rows, err := db.db.Query("SELECT ip_address FROM users WHERE username = ?", username)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
defer rows.Close()
var ip_address string
rows.Next()
rows.Scan(&ip_address)
return ip_address
} }
func (db *Database) UserGetTimezone(username string) string { func (db *Database) UserGetTimezone(ip_address string) string {
rows, err := db.db.Query("SELECT timezone FROM users WHERE username = ?", username) rows, err := db.db.Query("SELECT timezone FROM users WHERE ip_address = ?", ip_address)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
@ -128,14 +130,16 @@ func (db *Database) UsersGet() []User {
var users []User var users []User
for rows.Next() { for rows.Next() {
var id string var id string
var ip_address string
var username string var username string
var created_at string var created_at string
var timezone string var timezone string
rows.Scan(&id, &username, &created_at, &timezone) rows.Scan(&id, &ip_address, &username, &created_at, &timezone)
user := User{ user := User{
Id: id, Id: id,
Username: username, Username: username,
Timezone: timezone, IpAddress: ip_address,
Timezone: timezone,
} }
users = append(users, user) users = append(users, user)
} }
@ -144,14 +148,11 @@ func (db *Database) UsersGet() []User {
func (db *Database) MessagesGet() []Message { func (db *Database) MessagesGet() []Message {
rows, err := db.db.Query(` rows, err := db.db.Query(`
SELECT SELECT messages.id, messages.ip_address, messages.content,
messages.id, strftime('%Y-%m-%d %H:%M:%S', messages.created_at) as created_at,
messages.username, users.username, messages.edited
messages.content, FROM messages
strftime('%Y-%m-%d %H:%M:%S', messages.created_at) as created_at, LEFT JOIN users ON messages.ip_address = users.ip_address;
messages.edited
FROM
messages
`) `)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -163,10 +164,11 @@ func (db *Database) MessagesGet() []Message {
for rows.Next() { for rows.Next() {
var id string var id string
var content string var content string
var ip_address string
var created_at string var created_at string
var username string var username string
var edited int var edited int
rows.Scan(&id, &username, &content, &created_at, &edited) rows.Scan(&id, &ip_address, &content, &created_at, &username, &edited)
editedBool := false editedBool := false
if edited == 1 { if edited == 1 {
@ -176,6 +178,7 @@ func (db *Database) MessagesGet() []Message {
message := Message{ message := Message{
Id: id, Id: id,
Content: content, Content: content,
SenderIp: ip_address,
SenderUsername: username, SenderUsername: username,
Edited: editedBool, Edited: editedBool,
Timestamp: created_at, Timestamp: created_at,
@ -195,8 +198,8 @@ func (db *Database) UserNameExists(username string) bool {
return rows.Next() return rows.Next()
} }
func (db *Database) UserExists(username string) bool { func (db *Database) UserExists(ip string) bool {
rows, err := db.db.Query("SELECT * FROM users WHERE username = ?", username) rows, err := db.db.Query("SELECT * FROM users WHERE ip_address = ?", ip)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
@ -204,21 +207,28 @@ func (db *Database) UserExists(username string) bool {
return rows.Next() return rows.Next()
} }
func (db *Database) UserNameChange(oldUsername, newUsername string) { func (db *Database) UserNameChange(ip, newUsername string) {
_, err := db.db.Exec("UPDATE users SET username = ? WHERE username = ?", newUsername, oldUsername) _, err := db.db.Exec("UPDATE users SET username = ? WHERE ip_address = ?", newUsername, ip)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
} }
func (db *Database) UserMessagesGet(username string) []Message { 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 {
rows, err := db.db.Query(` rows, err := db.db.Query(`
SELECT messages.*, users.username SELECT messages.*, users.username
FROM messages FROM messages
LEFT JOIN users ON messages.username = users.username LEFT JOIN users ON messages.ip_address = users.ip_address
WHERE messages.username = ? WHERE messages.ip_address = ?
ORDER BY messages.created_at DESC; ORDER BY messages.created_at DESC;
`, username) `, ip)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
@ -229,10 +239,11 @@ func (db *Database) UserMessagesGet(username string) []Message {
for rows.Next() { for rows.Next() {
var id string var id string
var content string var content string
var ip_address string
var created_at string var created_at string
var username string var username string
var edited int var edited int
rows.Scan(&id, &content, &created_at, &username, &edited) rows.Scan(&id, &ip_address, &content, &created_at, &username, &edited)
t, _ := time.Parse(created_at, created_at) t, _ := time.Parse(created_at, created_at)
editedBool := false editedBool := false
if edited == 1 { if edited == 1 {
@ -241,6 +252,7 @@ func (db *Database) UserMessagesGet(username string) []Message {
message := Message{ message := Message{
Id: id, Id: id,
Content: content, Content: content,
SenderIp: ip_address,
SenderUsername: username, SenderUsername: username,
Edited: editedBool, Edited: editedBool,
Timestamp: t.Format(created_at), Timestamp: t.Format(created_at),
@ -256,8 +268,8 @@ func (db *Database) MessageDeleteId(id string) {
fmt.Println(err) fmt.Println(err)
} }
} }
func (db *Database) MessageDeleteIfOwner(id string, username string) (int, error) { func (db *Database) MessageDeleteIfOwner(id string, ip string) (int, error) {
res, err := db.db.Exec("DELETE FROM messages WHERE id = ? AND username = ?", id, username) res, err := db.db.Exec("DELETE FROM messages WHERE id = ? AND ip_address = ?", id, ip)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -269,8 +281,8 @@ func (db *Database) MessageDeleteIfOwner(id string, username string) (int, error
} }
func (db *Database) MessageEditIfOwner(id string, content string, username 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 username = ?", content, id, username) res, err := db.db.Exec("UPDATE messages SET content = ?, edited = 1 WHERE id = ? AND ip_address = ?", content, id, ip)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -292,8 +304,8 @@ func (db *Database) DeleteOldMessages(ageMinutes int) {
} }
} }
func (db *Database) UserDelete(username string) { func (db *Database) UserDeleteIp(ip string) {
_, err := db.db.Exec("DELETE FROM users WHERE username = ?", username) _, err := db.db.Exec("DELETE FROM users WHERE ip_address = ?", ip)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }

2
go.mod
View File

@ -3,5 +3,3 @@ module chat
go 1.23.4 go 1.23.4
require github.com/mattn/go-sqlite3 v1.14.24 // direct require github.com/mattn/go-sqlite3 v1.14.24 // direct
require golang.org/x/crypto v0.32.0 // indirect

2
go.sum
View File

@ -1,4 +1,2 @@
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 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= 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=

View File

@ -433,33 +433,19 @@ async function checkUsername() {
const response = await fetch("/username/status"); const response = await fetch("/username/status");
const data = await response.json(); const data = await response.json();
if (!data.hasUsername) { if (!data.hasUsername) {
// redirect to login page document.getElementById("settings-panel").style
window.location.href = "/login"; .display = "block";
// const username = document.getElementById("username");
// username.focus();
// document.getElementById("settings-panel").style username.selectionStart =
// .display = "block"; username.selectionEnd =
// const username = document.getElementById("username"); username.value.length;
// username.focus();
// username.selectionStart =
// username.selectionEnd =
// username.value.length;
} }
} catch (error) { } catch (error) {
console.error("Error checking username status:", 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() { async function updateCurrentUser() {
try { try {
const response = await fetch("/username/status"); const response = await fetch("/username/status");
@ -522,15 +508,15 @@ async function sendMessage() {
if (!message) { if (!message) {
return; return;
} }
try { try {
lastMessage = message; lastMessage = message;
const username = await getCurrentUsername();
const response = await fetch("/messages", { const response = await fetch("/messages", {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ username: username, message: message }), body: JSON.stringify({ message: message }),
}); });
const data = await response.json(); const data = await response.json();

View File

@ -1,178 +0,0 @@
<!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>

View File

@ -1,186 +0,0 @@
<!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>

View File

@ -12,15 +12,9 @@
## Backend ## Backend
### High Priority ### High Priority
- Lazy load with pagination (frontend and backend) - Lazy load with pagination (frontend and backend)
- Add actual logging instead of ip based usernames, have messages tied to the logged in user not an ip (db changes)
- Add live voice chat? (This will be fun, maybe a separate app) - Add live voice chat? (This will be fun, maybe a separate app)
### Mid Priority ### Mid Priority
- NEW LOGIN STUFF - Nothing yet
- 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 ### Low Priority
- Nothing yet - Nothing yet

View File

@ -4,21 +4,9 @@ import (
tu "chat/tu" tu "chat/tu"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/rand"
"net/http" "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) { func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
clientIP := getClientIP(r) clientIP := getClientIP(r)
s.updateActivity(clientIP) s.updateActivity(clientIP)
@ -45,32 +33,35 @@ func (s *Server) handleUsername(w http.ResponseWriter, r *http.Request) {
return return
} }
s.mu.Lock()
if len(req.Username) > s.Config.Options.NameMaxLength { 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) 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 return
} }
if !validUsername(req.Username) { if !validUsername(req.Username) {
http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest) http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
s.mu.Unlock()
return return
} }
if s.Database.UserNameExists(req.Username) { if s.Database.UserNameExists(req.Username) {
http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict) http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
s.mu.Unlock()
return return
} }
s.mu.Lock() if s.Database.UserExists(clientIP) {
defer s.mu.Unlock() s.Database.UserNameChange(clientIP, req.Username)
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 { } else {
http.Error(w, `{"error": "Failure to change username"}`, http.StatusUnauthorized) s.Database.UserAdd(clientIP, req.Username)
} }
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]string{"status": "Username registered"})
} }
func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) { func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
@ -106,11 +97,7 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
messages := s.Database.MessagesGet() messages := s.Database.MessagesGet()
for _, msg := range messages { for _, msg := range messages {
clientIP := getClientIP(r) clientIP := getClientIP(r)
username, ok := s.LoggedIn[clientIP] timeZone := s.Database.UserGetTimezone(clientIP)
timeZone := "UTC"
if ok {
timeZone = s.Database.UserGetTimezone(username)
}
timeLocal := tu.TimeStringToTimeInLocation(msg.Timestamp, timeZone) timeLocal := tu.TimeStringToTimeInLocation(msg.Timestamp, timeZone)
edited := "" edited := ""
if msg.Edited { if msg.Edited {
@ -124,9 +111,22 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
case http.MethodPut: case http.MethodPut:
w.Header().Set("Content-Type", "application/json") 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 { var msg struct {
Username string `json:"username"` Message string `json:"message"`
Message string `json:"message"`
} }
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil { if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
@ -134,19 +134,12 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
return return
} }
clientIP := getClientIP(r) s.Database.MessageAdd(clientIP, msg.Message)
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: case http.MethodDelete:
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -178,16 +171,21 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
} }
func (s *Server) handleUsernameStatus(w http.ResponseWriter, r *http.Request) { func (s *Server) handleUsernameStatus(w http.ResponseWriter, r *http.Request) {
// if r.Method != http.MethodGet { if r.Method != http.MethodGet {
// http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed) http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
// return return
// } }
clientIP := getClientIP(r)
username, ok := s.LoggedIn[clientIP]
w.Header().Set("Content-Type", "application/json") 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{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"hasUsername": ok, "hasUsername": exists,
"username": username, "username": username,
}) })
} }
@ -204,14 +202,12 @@ func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
s.updateActivity(clientIP) s.updateActivity(clientIP)
s.cleanupActivity() s.cleanupActivity()
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock()
var users []string var users []string
for ip := range s.Connected { for ip := range s.Connected {
// for all connected, get their usernames // for all connected, get their usernames
if username, ok := s.LoggedIn[ip]; ok { users = append(users, s.Database.UserNameGet(ip))
users = append(users, username)
}
} }
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"users": users, "users": users,
@ -237,15 +233,18 @@ func (s *Server) handleTimezone(w http.ResponseWriter, r *http.Request) {
} }
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock()
if username, ok := s.LoggedIn[clientIP]; ok { if !s.Database.UserExists(clientIP) {
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) 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) { func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
@ -284,90 +283,3 @@ func (s *Server) handleMessagesLength(w http.ResponseWriter, r *http.Request) {
"length": len(messages), "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",
})
}
}

View File

@ -21,17 +21,15 @@ type Config struct {
Port int `json:"port"` Port int `json:"port"`
} `json:"server"` } `json:"server"`
Paths struct { Paths struct {
DatabasePath string `json:"databasePath"` DatabasePath string `json:"databasePath"`
IndexJsPath string `json:"indexJsPath"` IndexJsPath string `json:"indexJsPath"`
IndexCssPath string `json:"indexCssPath"` IndexCssPath string `json:"indexCssPath"`
IndexHtmlPath string `json:"indexHtmlPath"` IndexHtmlPath string `json:"indexHtmlPath"`
SignupHtmlPath string `json:"signupHtmlPath"` MessagesHtmlPath string `json:"messagesHtmlPath"`
LoginHtmlPath string `json:"loginHtmlPath"`
} `json:"paths"` } `json:"paths"`
Options struct { Options struct {
MessageMaxAge int `json:"messageMaxAge"` MessageMaxAge int `json:"messageMaxAge"`
NameMaxLength int `json:"nameMaxLength"` NameMaxLength int `json:"nameMaxLength"`
MessagePerPage int `json:"messagePerPage"`
} `json:"options"` } `json:"options"`
} }
@ -42,8 +40,7 @@ func LoadConfig(filepath string) Config {
config.Paths.IndexHtmlPath = pathMaker(config.Paths.IndexHtmlPath) config.Paths.IndexHtmlPath = pathMaker(config.Paths.IndexHtmlPath)
config.Paths.IndexJsPath = pathMaker(config.Paths.IndexJsPath) config.Paths.IndexJsPath = pathMaker(config.Paths.IndexJsPath)
config.Paths.IndexCssPath = pathMaker(config.Paths.IndexCssPath) config.Paths.IndexCssPath = pathMaker(config.Paths.IndexCssPath)
config.Paths.SignupHtmlPath = pathMaker(config.Paths.SignupHtmlPath) config.Paths.MessagesHtmlPath = pathMaker(config.Paths.MessagesHtmlPath)
config.Paths.LoginHtmlPath = pathMaker(config.Paths.LoginHtmlPath)
config.Paths.DatabasePath = pathMaker(config.Paths.DatabasePath) config.Paths.DatabasePath = pathMaker(config.Paths.DatabasePath)
if err != nil { if err != nil {
fmt.Println("Error parsing config file: ", err) fmt.Println("Error parsing config file: ", err)
@ -121,7 +118,6 @@ func validUsername(username string) bool {
} }
type Server struct { type Server struct {
LoggedIn map[string]string // Map Username -> IP
Connected map[string]time.Time // Map IP -> Last activity time Connected map[string]time.Time // Map IP -> Last activity time
Database *db.Database Database *db.Database
Config Config Config Config
@ -131,7 +127,6 @@ type Server struct {
func NewServer(config Config) *Server { func NewServer(config Config) *Server {
return &Server{ return &Server{
LoggedIn: make(map[string]string),
Connected: make(map[string]time.Time), Connected: make(map[string]time.Time),
Database: db.OpenDatabase(config.Paths.DatabasePath), Database: db.OpenDatabase(config.Paths.DatabasePath),
Config: config, Config: config,
@ -143,23 +138,6 @@ func (s *Server) AddMessage(userip string, contents string) {
s.Database.MessageAdd(userip, contents) 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) { func (s *Server) updateActivity(ip string) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -172,7 +150,6 @@ func (s *Server) cleanupActivity() {
for ip, lastActivity := range s.Connected { for ip, lastActivity := range s.Connected {
if time.Since(lastActivity) > 10*time.Second { if time.Since(lastActivity) > 10*time.Second {
delete(s.Connected, ip) delete(s.Connected, ip)
s.LogUserOut(s.LoggedIn[ip])
} }
} }
} }
@ -192,8 +169,6 @@ func (s *Server) Run() {
handler.HandleFunc("/users", s.handleUsers) handler.HandleFunc("/users", s.handleUsers)
handler.HandleFunc("/username", s.handleUsername) handler.HandleFunc("/username", s.handleUsername)
handler.HandleFunc("/messages", s.handleMessages) 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) fmt.Printf("Server starting on %s:%d\n", s.Config.Server.IpAddress, s.Config.Server.Port)
defer s.Stop() defer s.Stop()
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Config.Server.IpAddress, s.Config.Server.Port), GzipMiddleware(handler)); err != nil { if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Config.Server.IpAddress, s.Config.Server.Port), GzipMiddleware(handler)); err != nil {