Compare commits

..

No commits in common. "jocadbz" and "master" have entirely different histories.

12 changed files with 267 additions and 404 deletions

3
.gitignore vendored
View File

@ -1,8 +1,5 @@
config/config.json config/config.json
config/about_page.htmlbody config/about_page.htmlbody
# Testing
files/
# nano # nano
.swp .swp

View File

@ -4,6 +4,5 @@
"db_username": "threadr_user", "db_username": "threadr_user",
"db_password": "threadr_password", "db_password": "threadr_password",
"db_database": "threadr_db", "db_database": "threadr_db",
"db_svr_host": "localhost:3306", "db_svr_host": "localhost:3306"
"file_storage_dir": "files"
} }

View File

@ -1,13 +1,11 @@
package handlers package handlers
import ( import (
"context" "context"
"database/sql" "database/sql"
"html/template" "html/template"
"log" "net/http"
"net/http" "github.com/gorilla/sessions"
"github.com/gorilla/sessions"
) )
type PageData struct { type PageData struct {
@ -21,13 +19,12 @@ type PageData struct {
} }
type Config struct { type Config struct {
DomainName string `json:"domain_name"` DomainName string `json:"domain_name"`
ThreadrDir string `json:"threadr_dir"` ThreadrDir string `json:"threadr_dir"`
DBUsername string `json:"db_username"` DBUsername string `json:"db_username"`
DBPassword string `json:"db_password"` DBPassword string `json:"db_password"`
DBDatabase string `json:"db_database"` DBDatabase string `json:"db_database"`
DBServerHost string `json:"db_svr_host"` DBServerHost string `json:"db_svr_host"`
FileStorageDir string `json:"file_storage_dir"`
} }
type App struct { type App struct {
@ -48,21 +45,24 @@ func (app *App) SessionMW(next http.HandlerFunc) http.HandlerFunc {
HttpOnly: true, HttpOnly: true,
} }
} }
if _, ok := session.Values["user_id"].(int); ok {
ctx := context.WithValue(r.Context(), "session", session) // Skip IP and User-Agent check for WebSocket connections
r = r.WithContext(ctx) if r.URL.Query().Get("ws") != "true" {
if session.Values["user_ip"] != r.RemoteAddr || session.Values["user_agent"] != r.UserAgent() {
next(w, r) session.Values = make(map[interface{}]interface{})
session.Options.MaxAge = -1
if err := session.Save(r, w); err != nil { session.Save(r, w)
/* http.Redirect(w, r, app.Config.ThreadrDir+"/login/?error=session", http.StatusFound)
Ok, so here's the thing return
Errors coming from this function here "can" be ignored. }
They mostly come from errors while setting cookies, so in some }
environments this will trigger a lot, but they are harmless. ctx := context.WithValue(r.Context(), "session", session)
*/ r = r.WithContext(ctx)
log.Printf("Error saving session in SessionMW: %v", err) } else {
ctx := context.WithValue(r.Context(), "session", session)
r = r.WithContext(ctx)
} }
next(w, r)
} }
} }

View File

@ -1,32 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"path/filepath"
"strconv"
"threadr/models"
)
func FileHandler(app *App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fileIDStr := r.URL.Query().Get("id")
fileID, err := strconv.ParseInt(fileIDStr, 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
file, err := models.GetFileByID(app.DB, fileID)
if err != nil || file == nil {
http.NotFound(w, r)
return
}
fileExt := filepath.Ext(file.OriginalName)
fileName := fmt.Sprintf("%d%s", fileID, fileExt)
filePath := filepath.Join(app.Config.FileStorageDir, fileName)
http.ServeFile(w, r, filePath)
}
}

View File

@ -1,99 +1,34 @@
package handlers package handlers
import ( import (
"crypto/sha256" "log"
"fmt" "net/http"
"io" "threadr/models"
"log" "github.com/gorilla/sessions"
"net/http"
"os"
"path/filepath"
"threadr/models"
"github.com/gorilla/sessions"
) )
func ProfileEditHandler(app *App) http.HandlerFunc { func ProfileEditHandler(app *App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*sessions.Session) session := r.Context().Value("session").(*sessions.Session)
userID, ok := session.Values["user_id"].(int) userID, ok := session.Values["user_id"].(int)
if !ok { if !ok {
http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound)
return return
} }
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
// Handle file upload displayName := r.FormValue("display_name")
file, handler, err := r.FormFile("pfp") pfpURL := r.FormValue("pfp_url")
if err == nil { bio := r.FormValue("bio")
defer file.Close() err := models.UpdateUserProfile(app.DB, userID, displayName, pfpURL, bio)
if err != nil {
// Create a hash of the file log.Printf("Error updating profile: %v", err)
h := sha256.New() http.Error(w, "Failed to update profile", http.StatusInternalServerError)
if _, err := io.Copy(h, file); err != nil { return
log.Printf("Error hashing file: %v", err) }
http.Error(w, "Failed to process file", http.StatusInternalServerError) http.Redirect(w, r, app.Config.ThreadrDir+"/profile/", http.StatusFound)
return return
} }
fileHash := fmt.Sprintf("%x", h.Sum(nil))
// Create file record in the database
fileRecord := models.File{
OriginalName: handler.Filename,
Hash: fileHash,
HashAlgorithm: "sha256",
}
fileID, err := models.CreateFile(app.DB, fileRecord)
if err != nil {
log.Printf("Error creating file record: %v", err)
http.Error(w, "Failed to save file information", http.StatusInternalServerError)
return
}
// Save the file to disk
fileExt := filepath.Ext(handler.Filename)
newFileName := fmt.Sprintf("%d%s", fileID, fileExt)
filePath := filepath.Join(app.Config.FileStorageDir, newFileName)
// Reset file pointer
file.Seek(0, 0)
dst, err := os.Create(filePath)
if err != nil {
log.Printf("Error creating file on disk: %v", err)
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
log.Printf("Error saving file to disk: %v", err)
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
// Update user's pfp_file_id
err = models.UpdateUserPfp(app.DB, userID, fileID)
if err != nil {
log.Printf("Error updating user pfp: %v", err)
http.Error(w, "Failed to update profile", http.StatusInternalServerError)
return
}
}
// Update other profile fields
displayName := r.FormValue("display_name")
bio := r.FormValue("bio")
err = models.UpdateUserProfile(app.DB, userID, displayName, bio)
if err != nil {
log.Printf("Error updating profile: %v", err)
http.Error(w, "Failed to update profile", http.StatusInternalServerError)
return
}
http.Redirect(w, r, app.Config.ThreadrDir+"/profile/", http.StatusFound)
return
}
user, err := models.GetUserByID(app.DB, userID) user, err := models.GetUserByID(app.DB, userID)
if err != nil { if err != nil {

64
main.go
View File

@ -49,6 +49,26 @@ func createTablesIfNotExist(db *sql.DB) error {
return fmt.Errorf("error creating boards table: %v", err) return fmt.Errorf("error creating boards table: %v", err)
} }
// Create users table
_, err = db.Exec(`
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
display_name VARCHAR(255),
pfp_url VARCHAR(255),
bio TEXT,
authentication_string VARCHAR(128) NOT NULL,
authentication_salt VARCHAR(255) NOT NULL,
authentication_algorithm VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
verified BOOLEAN DEFAULT FALSE,
permissions BIGINT DEFAULT 0
)`)
if err != nil {
return fmt.Errorf("error creating users table: %v", err)
}
// Create threads table (without type field) // Create threads table (without type field)
_, err = db.Exec(` _, err = db.Exec(`
CREATE TABLE threads ( CREATE TABLE threads (
@ -179,41 +199,6 @@ func createTablesIfNotExist(db *sql.DB) error {
return fmt.Errorf("error creating chat_messages table: %v", err) return fmt.Errorf("error creating chat_messages table: %v", err)
} }
// Create files table (Hope this does not break anything)
_, err = db.Exec(`
CREATE TABLE files (
id INT AUTO_INCREMENT PRIMARY KEY,
original_name VARCHAR(255) NOT NULL,
hash VARCHAR(255) NOT NULL,
hash_algorithm VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
return fmt.Errorf("error creating files table: %v", err)
}
// Create users table (KEEP THIS HERE!)
// Otherwise SQL bitches about the foreign key.
_, err = db.Exec(`
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
display_name VARCHAR(255),
pfp_file_id INT,
bio TEXT,
authentication_string VARCHAR(128) NOT NULL,
authentication_salt VARCHAR(255) NOT NULL,
authentication_algorithm VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
verified BOOLEAN DEFAULT FALSE,
permissions BIGINT DEFAULT 0,
FOREIGN KEY (pfp_file_id) REFERENCES files(id)
)`)
if err != nil {
return fmt.Errorf("error creating users table: %v", err)
}
log.Println("Database tables created.") log.Println("Database tables created.")
return nil return nil
} }
@ -298,14 +283,6 @@ func main() {
} }
defer db.Close() defer db.Close()
// Create the file directory
// TODO: Wouldn't this be better suited on the initialize function?
// Discussion pending.
err = os.MkdirAll(config.FileStorageDir, 0700)
if err != nil {
log.Fatal("Error creating file storage directory:", err)
}
// Perform initialization if the flag is set // Perform initialization if the flag is set
if *initialize { if *initialize {
log.Println("Initializing database...") log.Println("Initializing database...")
@ -383,7 +360,6 @@ func main() {
http.HandleFunc(config.ThreadrDir+"/signup/", app.SessionMW(handlers.SignupHandler(app))) http.HandleFunc(config.ThreadrDir+"/signup/", app.SessionMW(handlers.SignupHandler(app)))
http.HandleFunc(config.ThreadrDir+"/accept_cookie/", app.SessionMW(handlers.AcceptCookieHandler(app))) http.HandleFunc(config.ThreadrDir+"/accept_cookie/", app.SessionMW(handlers.AcceptCookieHandler(app)))
http.HandleFunc(config.ThreadrDir+"/chat/", app.SessionMW(app.RequireLoginMW(handlers.ChatHandler(app)))) http.HandleFunc(config.ThreadrDir+"/chat/", app.SessionMW(app.RequireLoginMW(handlers.ChatHandler(app))))
http.HandleFunc(config.ThreadrDir+"/file", app.SessionMW(handlers.FileHandler(app)))
log.Println("Server starting on :8080") log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))

View File

@ -2,19 +2,18 @@ package models
import ( import (
"database/sql" "database/sql"
"regexp"
"time" "time"
) )
type ChatMessage struct { type ChatMessage struct {
ID int `json:"id"` ID int
UserID int `json:"userId"` UserID int
Content string `json:"content"` Content string
ReplyTo int `json:"replyTo"` ReplyTo int // -1 if not a reply
Timestamp time.Time `json:"timestamp"` Timestamp time.Time
Username string `json:"username"` Username string // For display, fetched from user
PfpFileID sql.NullInt64 `json:"pfpFileId"` PfpURL string // For display, fetched from user
Mentions []string `json:"mentions"` Mentions []string // List of mentioned usernames
} }
func CreateChatMessage(db *sql.DB, msg ChatMessage) error { func CreateChatMessage(db *sql.DB, msg ChatMessage) error {
@ -25,7 +24,7 @@ func CreateChatMessage(db *sql.DB, msg ChatMessage) error {
func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) { func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) {
query := ` query := `
SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_url
FROM chat_messages cm FROM chat_messages cm
JOIN users u ON cm.user_id = u.id JOIN users u ON cm.user_id = u.id
ORDER BY cm.timestamp DESC ORDER BY cm.timestamp DESC
@ -40,7 +39,8 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) {
for rows.Next() { for rows.Next() {
var msg ChatMessage var msg ChatMessage
var timestampStr string var timestampStr string
err := rows.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, &timestampStr, &msg.Username, &msg.PfpFileID) var pfpURL sql.NullString
err := rows.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, &timestampStr, &msg.Username, &pfpURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -48,6 +48,10 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) {
if err != nil { if err != nil {
msg.Timestamp = time.Time{} msg.Timestamp = time.Time{}
} }
if pfpURL.Valid {
msg.PfpURL = pfpURL.String
}
// Parse mentions from content (simple @username detection)
msg.Mentions = extractMentions(msg.Content) msg.Mentions = extractMentions(msg.Content)
messages = append(messages, msg) messages = append(messages, msg)
} }
@ -56,14 +60,15 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) {
func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) { func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) {
query := ` query := `
SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_url
FROM chat_messages cm FROM chat_messages cm
JOIN users u ON cm.user_id = u.id JOIN users u ON cm.user_id = u.id
WHERE cm.id = ?` WHERE cm.id = ?`
row := db.QueryRow(query, id) row := db.QueryRow(query, id)
var msg ChatMessage var msg ChatMessage
var timestampStr string var timestampStr string
err := row.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, &timestampStr, &msg.Username, &msg.PfpFileID) var pfpURL sql.NullString
err := row.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, &timestampStr, &msg.Username, &pfpURL)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, nil return nil, nil
} }
@ -74,6 +79,9 @@ func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) {
if err != nil { if err != nil {
msg.Timestamp = time.Time{} msg.Timestamp = time.Time{}
} }
if pfpURL.Valid {
msg.PfpURL = pfpURL.String
}
msg.Mentions = extractMentions(msg.Content) msg.Mentions = extractMentions(msg.Content)
return &msg, nil return &msg, nil
} }
@ -99,11 +107,26 @@ func GetUsernamesMatching(db *sql.DB, prefix string) ([]string, error) {
// Simple utility to extract mentions from content // Simple utility to extract mentions from content
func extractMentions(content string) []string { func extractMentions(content string) []string {
re := regexp.MustCompile(`@(\w+)`) var mentions []string
matches := re.FindAllStringSubmatch(content, -1) var currentMention string
mentions := make([]string, len(matches)) inMention := false
for i, match := range matches {
mentions[i] = match[1] for _, char := range content {
if char == '@' {
inMention = true
currentMention = "@"
} else if inMention && (char == ' ' || char == '\n' || char == '\t') {
if len(currentMention) > 1 {
mentions = append(mentions, currentMention)
}
inMention = false
currentMention = ""
} else if inMention {
currentMention += string(char)
}
}
if inMention && len(currentMention) > 1 {
mentions = append(mentions, currentMention)
} }
return mentions return mentions
} }

View File

@ -1,35 +0,0 @@
package models
import (
"database/sql"
)
type File struct {
ID int
OriginalName string
Hash string
HashAlgorithm string
}
func GetFileByID(db *sql.DB, id int64) (*File, error) {
query := "SELECT id, original_name, hash, hash_algorithm FROM files WHERE id = ?"
row := db.QueryRow(query, id)
file := &File{}
err := row.Scan(&file.ID, &file.OriginalName, &file.Hash, &file.HashAlgorithm)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return file, nil
}
func CreateFile(db *sql.DB, file File) (int64, error) {
query := "INSERT INTO files (original_name, hash, hash_algorithm) VALUES (?, ?, ?)"
result, err := db.Exec(query, file.OriginalName, file.Hash, file.HashAlgorithm)
if err != nil {
return 0, err
}
return result.LastInsertId()
}

View File

@ -1,155 +1,161 @@
package models package models
import ( import (
"crypto/sha256" "crypto/sha256"
"database/sql" "database/sql"
"fmt" "fmt"
"time" "time"
) )
type User struct { type User struct {
ID int ID int
Username string Username string
DisplayName string DisplayName string
PfpFileID sql.NullInt64 PfpURL string
Bio string Bio string
AuthenticationString string AuthenticationString string
AuthenticationSalt string AuthenticationSalt string
AuthenticationAlgorithm string AuthenticationAlgorithm string
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
Verified bool Verified bool
Permissions int64 Permissions int64
} }
func GetUserByID(db *sql.DB, id int) (*User, error) { func GetUserByID(db *sql.DB, id int) (*User, error) {
query := "SELECT id, username, display_name, pfp_file_id, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE id = ?" query := "SELECT id, username, display_name, pfp_url, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE id = ?"
row := db.QueryRow(query, id) row := db.QueryRow(query, id)
user := &User{} user := &User{}
var displayName sql.NullString var displayName sql.NullString
var bio sql.NullString var pfpURL sql.NullString
var createdAtString sql.NullString var bio sql.NullString
var updatedAtString sql.NullString var createdAtString sql.NullString
err := row.Scan(&user.ID, &user.Username, &displayName, &user.PfpFileID, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions) var updatedAtString sql.NullString
if err == sql.ErrNoRows { err := row.Scan(&user.ID, &user.Username, &displayName, &pfpURL, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions)
return nil, nil if err == sql.ErrNoRows {
} return nil, nil
if err != nil { }
return nil, err if err != nil {
} return nil, err
if displayName.Valid { }
user.DisplayName = displayName.String if displayName.Valid {
} else { user.DisplayName = displayName.String
user.DisplayName = "" } else {
} user.DisplayName = ""
if bio.Valid { }
user.Bio = bio.String if pfpURL.Valid {
} else { user.PfpURL = pfpURL.String
user.Bio = "" } else {
} user.PfpURL = ""
if createdAtString.Valid { }
user.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtString.String) if bio.Valid {
if err != nil { user.Bio = bio.String
return nil, fmt.Errorf("error parsing created_at: %v", err) } else {
} user.Bio = ""
} else { }
user.CreatedAt = time.Time{} if createdAtString.Valid {
} user.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtString.String)
if updatedAtString.Valid { if err != nil {
user.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtString.String) return nil, fmt.Errorf("error parsing created_at: %v", err)
if err != nil { }
return nil, fmt.Errorf("error parsing updated_at: %v", err) } else {
} user.CreatedAt = time.Time{}
} else { }
user.UpdatedAt = time.Time{} if updatedAtString.Valid {
} user.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtString.String)
return user, nil if err != nil {
return nil, fmt.Errorf("error parsing updated_at: %v", err)
}
} else {
user.UpdatedAt = time.Time{}
}
return user, nil
} }
func GetUserByUsername(db *sql.DB, username string) (*User, error) { func GetUserByUsername(db *sql.DB, username string) (*User, error) {
query := "SELECT id, username, display_name, pfp_file_id, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE username = ?" query := "SELECT id, username, display_name, pfp_url, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE username = ?"
row := db.QueryRow(query, username) row := db.QueryRow(query, username)
user := &User{} user := &User{}
var displayName sql.NullString var displayName sql.NullString
var bio sql.NullString var pfpURL sql.NullString
var createdAtString sql.NullString var bio sql.NullString
var updatedAtString sql.NullString var createdAtString sql.NullString
err := row.Scan(&user.ID, &user.Username, &displayName, &user.PfpFileID, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions) var updatedAtString sql.NullString
if err != nil { err := row.Scan(&user.ID, &user.Username, &displayName, &pfpURL, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions)
return nil, err if err != nil {
} return nil, err
if displayName.Valid { }
user.DisplayName = displayName.String if displayName.Valid {
} else { user.DisplayName = displayName.String
user.DisplayName = "" } else {
} user.DisplayName = ""
if bio.Valid { }
user.Bio = bio.String if pfpURL.Valid {
} else { user.PfpURL = pfpURL.String
user.Bio = "" } else {
} user.PfpURL = ""
if createdAtString.Valid { }
user.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtString.String) if bio.Valid {
if err != nil { user.Bio = bio.String
return nil, fmt.Errorf("error parsing created_at: %v", err) } else {
} user.Bio = ""
} else { }
user.CreatedAt = time.Time{} if createdAtString.Valid {
} user.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtString.String)
if updatedAtString.Valid { if err != nil {
user.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtString.String) return nil, fmt.Errorf("error parsing created_at: %v", err)
if err != nil { }
return nil, fmt.Errorf("error parsing updated_at: %v", err) } else {
} user.CreatedAt = time.Time{}
} else { }
user.UpdatedAt = time.Time{} if updatedAtString.Valid {
} user.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtString.String)
return user, nil if err != nil {
return nil, fmt.Errorf("error parsing updated_at: %v", err)
}
} else {
user.UpdatedAt = time.Time{}
}
return user, nil
} }
func CheckPassword(password, salt, algorithm, hash string) bool { func CheckPassword(password, salt, algorithm, hash string) bool {
if algorithm != "sha256" { if algorithm != "sha256" {
return false return false
} }
computedHash := HashPassword(password, salt, algorithm) computedHash := HashPassword(password, salt, algorithm)
return computedHash == hash return computedHash == hash
} }
func HashPassword(password, salt, algorithm string) string { func HashPassword(password, salt, algorithm string) string {
if algorithm != "sha256" { if algorithm != "sha256" {
return "" return ""
} }
data := password + salt data := password + salt
hash := sha256.Sum256([]byte(data)) hash := sha256.Sum256([]byte(data))
return fmt.Sprintf("%x", hash) return fmt.Sprintf("%x", hash)
} }
func CreateUser(db *sql.DB, username, password string) error { func CreateUser(db *sql.DB, username, password string) error {
salt := "random-salt" // Replace with secure random generation salt := "random-salt" // Replace with secure random generation
algorithm := "sha256" algorithm := "sha256"
hash := HashPassword(password, salt, algorithm) hash := HashPassword(password, salt, algorithm)
query := "INSERT INTO users (username, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions) VALUES (?, ?, ?, ?, NOW(), NOW(), ?, 0)" query := "INSERT INTO users (username, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions) VALUES (?, ?, ?, ?, NOW(), NOW(), ?, 0)"
_, err := db.Exec(query, username, hash, salt, algorithm, false) _, err := db.Exec(query, username, hash, salt, algorithm, false)
return err return err
} }
func UpdateUserProfile(db *sql.DB, userID int, displayName, bio string) error { func UpdateUserProfile(db *sql.DB, userID int, displayName, pfpURL, bio string) error {
query := "UPDATE users SET display_name = ?, bio = ?, updated_at = NOW() WHERE id = ?" query := "UPDATE users SET display_name = ?, pfp_url = ?, bio = ?, updated_at = NOW() WHERE id = ?"
_, err := db.Exec(query, displayName, bio, userID) _, err := db.Exec(query, displayName, pfpURL, bio, userID)
return err return err
}
func UpdateUserPfp(db *sql.DB, userID int, pfpFileID int64) error {
query := "UPDATE users SET pfp_file_id = ? WHERE id = ?"
_, err := db.Exec(query, pfpFileID, userID)
return err
} }
const ( const (
PermCreateBoard int64 = 1 << 0 PermCreateBoard int64 = 1 << 0
PermManageUsers int64 = 1 << 1 PermManageUsers int64 = 1 << 1
) )
func HasGlobalPermission(user *User, perm int64) bool { func HasGlobalPermission(user *User, perm int64) bool {
return user.Permissions&perm != 0 return user.Permissions&perm != 0
} }

View File

@ -224,8 +224,8 @@
{{range .Messages}} {{range .Messages}}
<div class="chat-message" id="msg-{{.ID}}"> <div class="chat-message" id="msg-{{.ID}}">
<div class="chat-message-header"> <div class="chat-message-header">
{{if .PfpFileID.Valid}} {{if .PfpURL}}
<img src="{{$.BasePath}}/file?id={{.PfpFileID.Int64}}" alt="PFP" class="chat-message-pfp"> <img src="{{.PfpURL}}" alt="PFP" class="chat-message-pfp">
{{else}} {{else}}
<div class="chat-message-pfp" style="background-color: #001858;"></div> <div class="chat-message-pfp" style="background-color: #001858;"></div>
{{end}} {{end}}
@ -262,7 +262,7 @@
let replyUsername = ''; let replyUsername = '';
function connectWebSocket() { function connectWebSocket() {
ws = new WebSocket('ws://' + window.location.host + '{{.BasePath}}/chat/?ws=true'); ws = new WebSocket('ws://' + window.location.host + '{{.BasePath}}/chat/?ws=true', [], { credentials: 'include' });
ws.onmessage = function(event) { ws.onmessage = function(event) {
const msg = JSON.parse(event.data); const msg = JSON.parse(event.data);
appendMessage(msg); appendMessage(msg);
@ -298,26 +298,21 @@
const messages = document.getElementById('chat-messages'); const messages = document.getElementById('chat-messages');
const msgDiv = document.createElement('div'); const msgDiv = document.createElement('div');
msgDiv.className = 'chat-message'; msgDiv.className = 'chat-message';
msgDiv.id = 'msg-' + msg.id; msgDiv.id = 'msg-' + msg.ID;
let pfpHTML = ''; let pfpHTML = msg.PfpURL ? `<img src="${msg.PfpURL}" alt="PFP" class="chat-message-pfp">` : `<div class="chat-message-pfp" style="background-color: #001858;"></div>`;
if (msg.pfpFileId && msg.pfpFileId.Valid) { let replyHTML = msg.ReplyTo > 0 ? `<div class="chat-message-reply" onclick="scrollToMessage(${msg.ReplyTo})">Replying to ${msg.Username}</div>` : '';
pfpHTML = `<img src="{{.BasePath}}/file?id=${msg.pfpFileId.Int64}&t=${new Date().getTime()}" alt="PFP" class="chat-message-pfp">`;
} else {
pfpHTML = `<div class="chat-message-pfp" style="background-color: #001858;"></div>`;
}
let replyHTML = msg.replyTo > 0 ? `<div class="chat-message-reply" onclick="scrollToMessage(${msg.replyTo})">Replying to ${msg.username}</div>` : '';
// Process content for mentions // Process content for mentions
let content = msg.content.replace(/@[\w]+/g, match => `<span class="chat-message-mention">${match}</span>`); let content = msg.Content.replace(/@[\w]+/g, match => `<span class="chat-message-mention">${match}</span>`);
msgDiv.innerHTML = ` msgDiv.innerHTML = `
<div class="chat-message-header"> <div class="chat-message-header">
${pfpHTML} ${pfpHTML}
<span class="chat-message-username">${msg.username}</span> <span class="chat-message-username">${msg.Username}</span>
<span class="chat-message-timestamp">${new Date(msg.timestamp).toLocaleString()}</span> <span class="chat-message-timestamp">${new Date(msg.Timestamp).toLocaleString()}</span>
</div> </div>
${replyHTML} ${replyHTML}
<div class="chat-message-content">${content}</div> <div class="chat-message-content">${content}</div>
<div class="post-actions"> <div class="post-actions">
<a href="javascript:void(0)" onclick="replyToMessage(${msg.id}, '${msg.username}')">Reply</a> <a href="javascript:void(0)" onclick="replyToMessage(${msg.ID}, '${msg.Username}')">Reply</a>
</div> </div>
`; `;
messages.appendChild(msgDiv); messages.appendChild(msgDiv);
@ -387,7 +382,6 @@
if (atIndex !== -1 && (caretPos === text.length || text[caretPos] === ' ')) { if (atIndex !== -1 && (caretPos === text.length || text[caretPos] === ' ')) {
const prefix = text.substring(atIndex + 1, caretPos); const prefix = text.substring(atIndex + 1, caretPos);
autocompletePrefix = prefix; autocompletePrefix = prefix;
// TODO: Fix this.
const response = await fetch('{{.BasePath}}/chat/?autocomplete=true&prefix=' + encodeURIComponent(prefix)); const response = await fetch('{{.BasePath}}/chat/?autocomplete=true&prefix=' + encodeURIComponent(prefix));
const usernames = await response.json(); const usernames = await response.json();
if (usernames.length > 0) { if (usernames.length > 0) {

View File

@ -14,8 +14,8 @@
<section> <section>
<p>Username: {{.User.Username}}</p> <p>Username: {{.User.Username}}</p>
<p>Display Name: {{.DisplayName}}</p> <p>Display Name: {{.DisplayName}}</p>
{{if .User.PfpFileID.Valid}} {{if .User.PfpURL}}
<img src="{{.BasePath}}/file?id={{.User.PfpFileID.Int64}}" alt="Profile Picture"> <img src="{{.User.PfpURL}}" alt="Profile Picture">
{{end}} {{end}}
<p>Bio: {{.User.Bio}}</p> <p>Bio: {{.User.Bio}}</p>
<p>Joined: {{.User.CreatedAt}}</p> <p>Joined: {{.User.CreatedAt}}</p>

View File

@ -12,11 +12,11 @@
<h2>Edit Profile</h2> <h2>Edit Profile</h2>
</header> </header>
<section> <section>
<form method="post" action="{{.BasePath}}/profile/edit/" enctype="multipart/form-data"> <form method="post" action="{{.BasePath}}/profile/edit/">
<label for="display_name">Display Name:</label> <label for="display_name">Display Name:</label>
<input type="text" id="display_name" name="display_name" value="{{.User.DisplayName}}"><br> <input type="text" id="display_name" name="display_name" value="{{.User.DisplayName}}"><br>
<label for="pfp">Profile Picture:</label> <label for="pfp_url">Profile Picture URL:</label>
<input type="file" id="pfp" name="pfp"><br> <input type="text" id="pfp_url" name="pfp_url" value="{{.User.PfpURL}}"><br>
<label for="bio">Bio:</label> <label for="bio">Bio:</label>
<textarea id="bio" name="bio">{{.User.Bio}}</textarea><br> <textarea id="bio" name="bio">{{.User.Bio}}</textarea><br>
<input type="submit" value="Save"> <input type="submit" value="Save">