overhaul changes initial commit
This commit is contained in:
parent
f1b0c35f66
commit
624877eb20
@ -4,13 +4,16 @@
|
|||||||
"port": 8080
|
"port": 8080
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"databasePath": "/home/radon/Documents/chattest.db",
|
"databasePath": "/home/radon/Documents/chat.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
118
db/db.go
118
db/db.go
@ -4,6 +4,7 @@ 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"
|
||||||
)
|
)
|
||||||
@ -11,13 +12,12 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
Id string
|
Id string
|
||||||
Username string
|
Username string
|
||||||
IpAddress string
|
|
||||||
Timezone string
|
Timezone string
|
||||||
|
HashedPassword 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,
|
||||||
ip_address TEXT NOT NULL,
|
username 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,61 +56,59 @@ 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(ip_address, timezone string) {
|
func (db *Database) UserTimezoneSet(username, timezone string) {
|
||||||
_, err := db.db.Exec("UPDATE users SET timezone = ? WHERE ip_address = ?", timezone, ip_address)
|
_, err := db.db.Exec("UPDATE users SET timezone = ? WHERE username = ?", timezone, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) UserAdd(ip_address, username string) {
|
func (db *Database) UserAddWithPassword(username, unhashedPwd string) error {
|
||||||
_, err := db.db.Exec("INSERT INTO users (username, ip_address) VALUES (?, ?)", username, ip_address)
|
// unhashedPwd can not be larger than 72 bytes
|
||||||
if err != nil {
|
if len(unhashedPwd) > 72 {
|
||||||
fmt.Println(err)
|
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) {
|
func (db *Database) UserPasswordCheck(username, unhashedPwd string) bool {
|
||||||
_, err := db.db.Exec("INSERT INTO messages (ip_address, content) VALUES (?, ?)", ip_address, content)
|
rows, err := db.db.Query("SELECT hashed_password FROM users WHERE username = ?", username)
|
||||||
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 username string
|
var hashedPwd string
|
||||||
rows.Next()
|
rows.Next()
|
||||||
rows.Scan(&username)
|
rows.Scan(&hashedPwd)
|
||||||
return username
|
err = bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(unhashedPwd))
|
||||||
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) UserIpGet(username string) string {
|
func (db *Database) MessageAdd(username string, content string) {
|
||||||
rows, err := db.db.Query("SELECT ip_address FROM users WHERE username = ?", username)
|
_, err := db.db.Exec("INSERT INTO messages (username, content) VALUES (?, ?)", username, content)
|
||||||
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(ip_address string) string {
|
func (db *Database) UserGetTimezone(username string) string {
|
||||||
rows, err := db.db.Query("SELECT timezone FROM users WHERE ip_address = ?", ip_address)
|
rows, err := db.db.Query("SELECT timezone FROM users WHERE username = ?", username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
@ -130,15 +128,13 @@ 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, &ip_address, &username, &created_at, &timezone)
|
rows.Scan(&id, &username, &created_at, &timezone)
|
||||||
user := User{
|
user := User{
|
||||||
Id: id,
|
Id: id,
|
||||||
Username: username,
|
Username: username,
|
||||||
IpAddress: ip_address,
|
|
||||||
Timezone: timezone,
|
Timezone: timezone,
|
||||||
}
|
}
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
@ -148,11 +144,14 @@ 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 messages.id, messages.ip_address, messages.content,
|
SELECT
|
||||||
|
messages.id,
|
||||||
|
messages.username,
|
||||||
|
messages.content,
|
||||||
strftime('%Y-%m-%d %H:%M:%S', messages.created_at) as created_at,
|
strftime('%Y-%m-%d %H:%M:%S', messages.created_at) as created_at,
|
||||||
users.username, messages.edited
|
messages.edited
|
||||||
FROM messages
|
FROM
|
||||||
LEFT JOIN users ON messages.ip_address = users.ip_address;
|
messages
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@ -164,11 +163,10 @@ 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, &ip_address, &content, &created_at, &username, &edited)
|
rows.Scan(&id, &username, &content, &created_at, &edited)
|
||||||
|
|
||||||
editedBool := false
|
editedBool := false
|
||||||
if edited == 1 {
|
if edited == 1 {
|
||||||
@ -178,7 +176,6 @@ 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,
|
||||||
@ -198,8 +195,8 @@ func (db *Database) UserNameExists(username string) bool {
|
|||||||
return rows.Next()
|
return rows.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) UserExists(ip string) bool {
|
func (db *Database) UserExists(username string) bool {
|
||||||
rows, err := db.db.Query("SELECT * FROM users WHERE ip_address = ?", ip)
|
rows, err := db.db.Query("SELECT * FROM users WHERE username = ?", username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
@ -207,28 +204,21 @@ func (db *Database) UserExists(ip string) bool {
|
|||||||
return rows.Next()
|
return rows.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) UserNameChange(ip, newUsername string) {
|
func (db *Database) UserNameChange(oldUsername, newUsername string) {
|
||||||
_, err := db.db.Exec("UPDATE users SET username = ? WHERE ip_address = ?", newUsername, ip)
|
_, err := db.db.Exec("UPDATE users SET username = ? WHERE username = ?", newUsername, oldUsername)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) UserMessagesDelete(ip string) {
|
func (db *Database) UserMessagesGet(username string) []Message {
|
||||||
_, 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.ip_address = users.ip_address
|
LEFT JOIN users ON messages.username = users.username
|
||||||
WHERE messages.ip_address = ?
|
WHERE messages.username = ?
|
||||||
ORDER BY messages.created_at DESC;
|
ORDER BY messages.created_at DESC;
|
||||||
`, ip)
|
`, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
@ -239,11 +229,10 @@ func (db *Database) UserMessagesGet(ip 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, &ip_address, &content, &created_at, &username, &edited)
|
rows.Scan(&id, &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 {
|
||||||
@ -252,7 +241,6 @@ func (db *Database) UserMessagesGet(ip 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),
|
||||||
@ -268,8 +256,8 @@ func (db *Database) MessageDeleteId(id string) {
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (db *Database) MessageDeleteIfOwner(id string, ip string) (int, error) {
|
func (db *Database) MessageDeleteIfOwner(id string, username string) (int, error) {
|
||||||
res, err := db.db.Exec("DELETE FROM messages WHERE id = ? AND ip_address = ?", id, ip)
|
res, err := db.db.Exec("DELETE FROM messages WHERE id = ? AND username = ?", id, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
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) {
|
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 ip_address = ?", content, id, ip)
|
res, err := db.db.Exec("UPDATE messages SET content = ?, edited = 1 WHERE id = ? AND username = ?", content, id, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@ -304,8 +292,8 @@ func (db *Database) DeleteOldMessages(ageMinutes int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) UserDeleteIp(ip string) {
|
func (db *Database) UserDelete(username string) {
|
||||||
_, err := db.db.Exec("DELETE FROM users WHERE ip_address = ?", ip)
|
_, err := db.db.Exec("DELETE FROM users WHERE username = ?", username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -3,3 +3,5 @@ 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
2
go.sum
@ -1,2 +1,4 @@
|
|||||||
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=
|
||||||
|
@ -433,19 +433,33 @@ 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) {
|
||||||
document.getElementById("settings-panel").style
|
// redirect to login page
|
||||||
.display = "block";
|
window.location.href = "/login";
|
||||||
const username = document.getElementById("username");
|
//
|
||||||
username.focus();
|
//
|
||||||
username.selectionStart =
|
// document.getElementById("settings-panel").style
|
||||||
username.selectionEnd =
|
// .display = "block";
|
||||||
username.value.length;
|
// const username = document.getElementById("username");
|
||||||
|
// 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");
|
||||||
@ -508,15 +522,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({ message: message }),
|
body: JSON.stringify({ username: username, message: message }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
178
public/login.html
Normal file
178
public/login.html
Normal file
@ -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>
|
||||||
|
|
||||||
|
|
186
public/signup.html
Normal file
186
public/signup.html
Normal file
@ -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>
|
||||||
|
|
||||||
|
|
@ -14,6 +14,13 @@
|
|||||||
- Lazy load with pagination (frontend and backend)
|
- Lazy load with pagination (frontend and backend)
|
||||||
- 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
|
||||||
- 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
|
### Low Priority
|
||||||
- Nothing yet
|
- Nothing yet
|
||||||
|
190
srv/handle.go
190
srv/handle.go
@ -4,9 +4,21 @@ 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)
|
||||||
@ -33,35 +45,32 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Database.UserExists(clientIP) {
|
s.mu.Lock()
|
||||||
s.Database.UserNameChange(clientIP, req.Username)
|
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 {
|
} 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) {
|
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()
|
messages := s.Database.MessagesGet()
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
clientIP := getClientIP(r)
|
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)
|
timeLocal := tu.TimeStringToTimeInLocation(msg.Timestamp, timeZone)
|
||||||
edited := ""
|
edited := ""
|
||||||
if msg.Edited {
|
if msg.Edited {
|
||||||
@ -111,21 +124,8 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,12 +134,19 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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{
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
"status": "Message received",
|
"status": "Message received",
|
||||||
"from": username,
|
"from": username,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
http.Error(w, `{"error": "Unauthorized"}`, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
w.Header().Set("Content-Type", "application/json")
|
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) {
|
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": exists,
|
"hasUsername": ok,
|
||||||
"username": username,
|
"username": username,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -202,12 +204,14 @@ 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
|
||||||
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{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"users": users,
|
"users": users,
|
||||||
@ -233,18 +237,15 @@ func (s *Server) handleTimezone(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
if !s.Database.UserExists(clientIP) {
|
if username, ok := s.LoggedIn[clientIP]; ok {
|
||||||
http.Error(w, `{"error": "User not registered"}`, http.StatusUnauthorized)
|
s.Database.UserTimezoneSet(username, req.Timezone)
|
||||||
s.mu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Database.UserTimezoneSet(clientIP, req.Timezone)
|
|
||||||
s.mu.Unlock()
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
http.Error(w, `{"error": "User not registered"}`, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
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),
|
"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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
29
srv/srv.go
29
srv/srv.go
@ -25,11 +25,13 @@ type Config struct {
|
|||||||
IndexJsPath string `json:"indexJsPath"`
|
IndexJsPath string `json:"indexJsPath"`
|
||||||
IndexCssPath string `json:"indexCssPath"`
|
IndexCssPath string `json:"indexCssPath"`
|
||||||
IndexHtmlPath string `json:"indexHtmlPath"`
|
IndexHtmlPath string `json:"indexHtmlPath"`
|
||||||
MessagesHtmlPath string `json:"messagesHtmlPath"`
|
SignupHtmlPath string `json:"signupHtmlPath"`
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +42,8 @@ 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.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)
|
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)
|
||||||
@ -118,6 +121,7 @@ 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
|
||||||
@ -127,6 +131,7 @@ 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,
|
||||||
@ -138,6 +143,23 @@ 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()
|
||||||
@ -150,6 +172,7 @@ 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])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,6 +192,8 @@ 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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user