From 7b0528ef369173c80e7a8e91aa04e545db6c112c Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Sat, 21 Jun 2025 16:21:21 -0300 Subject: [PATCH] feat: Add file-based avatar system - A new `files` table to store metadata about uploaded files, including original name and hash. - The `users` table is updated to reference a `pfp_file_id` from the new `files` table, removing the insecure `pfp_url` field. - A new `/file` endpoint and handler (`handlers/file.go`) are created to serve files securely based on their ID, preventing direct file system access. - Profile editing (`handlers/profile_edit.go` and `templates/pages/profile_edit.html`) is updated to handle file uploads instead of URL inputs. - The chat feature (`models/chat.go` and `templates/pages/chat.html`) is updated to work with the new file ID system, ensuring avatars are displayed correctly. Should also fix #68. --- .gitignore | 3 + config/config.json.sample | 3 +- handlers/app.go | 13 +- handlers/file.go | 32 ++++ handlers/profile_edit.go | 115 ++++++++++--- main.go | 64 +++++--- models/chat.go | 61 +++---- models/file.go | 35 ++++ models/user.go | 258 +++++++++++++++--------------- templates/pages/chat.html | 28 ++-- templates/pages/profile.html | 6 +- templates/pages/profile_edit.html | 8 +- 12 files changed, 382 insertions(+), 244 deletions(-) create mode 100644 handlers/file.go create mode 100644 models/file.go diff --git a/.gitignore b/.gitignore index 2b677ff..836673d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ config/config.json config/about_page.htmlbody +# Testing +files/ + # nano .swp diff --git a/config/config.json.sample b/config/config.json.sample index 25c325f..bb96f0a 100644 --- a/config/config.json.sample +++ b/config/config.json.sample @@ -4,5 +4,6 @@ "db_username": "threadr_user", "db_password": "threadr_password", "db_database": "threadr_db", - "db_svr_host": "localhost:3306" + "db_svr_host": "localhost:3306", + "file_storage_dir": "files" } diff --git a/handlers/app.go b/handlers/app.go index 20cca0e..c8fbc6c 100644 --- a/handlers/app.go +++ b/handlers/app.go @@ -19,12 +19,13 @@ type PageData struct { } type Config struct { - DomainName string `json:"domain_name"` - ThreadrDir string `json:"threadr_dir"` - DBUsername string `json:"db_username"` - DBPassword string `json:"db_password"` - DBDatabase string `json:"db_database"` - DBServerHost string `json:"db_svr_host"` + DomainName string `json:"domain_name"` + ThreadrDir string `json:"threadr_dir"` + DBUsername string `json:"db_username"` + DBPassword string `json:"db_password"` + DBDatabase string `json:"db_database"` + DBServerHost string `json:"db_svr_host"` + FileStorageDir string `json:"file_storage_dir"` } type App struct { diff --git a/handlers/file.go b/handlers/file.go new file mode 100644 index 0000000..93279f4 --- /dev/null +++ b/handlers/file.go @@ -0,0 +1,32 @@ +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) + } +} diff --git a/handlers/profile_edit.go b/handlers/profile_edit.go index c0d29fc..38a7483 100644 --- a/handlers/profile_edit.go +++ b/handlers/profile_edit.go @@ -1,34 +1,99 @@ package handlers import ( - "log" - "net/http" - "threadr/models" - "github.com/gorilla/sessions" + "crypto/sha256" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "threadr/models" + + "github.com/gorilla/sessions" ) func ProfileEditHandler(app *App) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*sessions.Session) - userID, ok := session.Values["user_id"].(int) - if !ok { - http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) - return - } + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + userID, ok := session.Values["user_id"].(int) + if !ok { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) + return + } - if r.Method == http.MethodPost { - displayName := r.FormValue("display_name") - pfpURL := r.FormValue("pfp_url") - bio := r.FormValue("bio") - err := models.UpdateUserProfile(app.DB, userID, displayName, pfpURL, 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 - } + if r.Method == http.MethodPost { + // Handle file upload + file, handler, err := r.FormFile("pfp") + if err == nil { + defer file.Close() + + // Create a hash of the file + h := sha256.New() + if _, err := io.Copy(h, file); err != nil { + log.Printf("Error hashing file: %v", err) + http.Error(w, "Failed to process file", http.StatusInternalServerError) + 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) if err != nil { @@ -62,4 +127,4 @@ func ProfileEditHandler(app *App) http.HandlerFunc { return } } -} \ No newline at end of file +} diff --git a/main.go b/main.go index 3e37972..0fc557c 100644 --- a/main.go +++ b/main.go @@ -49,26 +49,6 @@ func createTablesIfNotExist(db *sql.DB) error { 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) _, err = db.Exec(` CREATE TABLE threads ( @@ -199,6 +179,41 @@ func createTablesIfNotExist(db *sql.DB) error { 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.") return nil } @@ -283,6 +298,14 @@ func main() { } 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 if *initialize { log.Println("Initializing database...") @@ -360,6 +383,7 @@ func main() { 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+"/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.Fatal(http.ListenAndServe(":8080", nil)) diff --git a/models/chat.go b/models/chat.go index 7e33565..e9045bf 100644 --- a/models/chat.go +++ b/models/chat.go @@ -2,18 +2,19 @@ package models import ( "database/sql" + "regexp" "time" ) type ChatMessage struct { - ID int - UserID int - Content string - ReplyTo int // -1 if not a reply - Timestamp time.Time - Username string // For display, fetched from user - PfpURL string // For display, fetched from user - Mentions []string // List of mentioned usernames + ID int `json:"id"` + UserID int `json:"userId"` + Content string `json:"content"` + ReplyTo int `json:"replyTo"` + Timestamp time.Time `json:"timestamp"` + Username string `json:"username"` + PfpFileID sql.NullInt64 `json:"pfpFileId"` + Mentions []string `json:"mentions"` } func CreateChatMessage(db *sql.DB, msg ChatMessage) error { @@ -24,7 +25,7 @@ func CreateChatMessage(db *sql.DB, msg ChatMessage) error { func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) { query := ` - SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_url + SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id FROM chat_messages cm JOIN users u ON cm.user_id = u.id ORDER BY cm.timestamp DESC @@ -39,8 +40,7 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) { for rows.Next() { var msg ChatMessage var timestampStr string - var pfpURL sql.NullString - err := rows.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, ×tampStr, &msg.Username, &pfpURL) + err := rows.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID) if err != nil { return nil, err } @@ -48,10 +48,6 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) { if err != nil { msg.Timestamp = time.Time{} } - if pfpURL.Valid { - msg.PfpURL = pfpURL.String - } - // Parse mentions from content (simple @username detection) msg.Mentions = extractMentions(msg.Content) messages = append(messages, msg) } @@ -60,15 +56,14 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) { func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) { query := ` - SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_url + SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id FROM chat_messages cm JOIN users u ON cm.user_id = u.id WHERE cm.id = ?` row := db.QueryRow(query, id) var msg ChatMessage var timestampStr string - var pfpURL sql.NullString - err := row.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, ×tampStr, &msg.Username, &pfpURL) + err := row.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID) if err == sql.ErrNoRows { return nil, nil } @@ -79,9 +74,6 @@ func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) { if err != nil { msg.Timestamp = time.Time{} } - if pfpURL.Valid { - msg.PfpURL = pfpURL.String - } msg.Mentions = extractMentions(msg.Content) return &msg, nil } @@ -107,26 +99,11 @@ func GetUsernamesMatching(db *sql.DB, prefix string) ([]string, error) { // Simple utility to extract mentions from content func extractMentions(content string) []string { - var mentions []string - var currentMention string - inMention := false - - 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) + re := regexp.MustCompile(`@(\w+)`) + matches := re.FindAllStringSubmatch(content, -1) + mentions := make([]string, len(matches)) + for i, match := range matches { + mentions[i] = match[1] } return mentions -} \ No newline at end of file +} diff --git a/models/file.go b/models/file.go new file mode 100644 index 0000000..10fffa5 --- /dev/null +++ b/models/file.go @@ -0,0 +1,35 @@ +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() +} diff --git a/models/user.go b/models/user.go index 1c6d03a..ab5c293 100644 --- a/models/user.go +++ b/models/user.go @@ -1,161 +1,155 @@ package models import ( - "crypto/sha256" - "database/sql" - "fmt" - "time" + "crypto/sha256" + "database/sql" + "fmt" + "time" ) type User struct { - ID int - Username string - DisplayName string - PfpURL string - Bio string - AuthenticationString string - AuthenticationSalt string - AuthenticationAlgorithm string - CreatedAt time.Time - UpdatedAt time.Time - Verified bool - Permissions int64 + ID int + Username string + DisplayName string + PfpFileID sql.NullInt64 + Bio string + AuthenticationString string + AuthenticationSalt string + AuthenticationAlgorithm string + CreatedAt time.Time + UpdatedAt time.Time + Verified bool + Permissions int64 } func GetUserByID(db *sql.DB, id int) (*User, error) { - 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) - user := &User{} - var displayName sql.NullString - var pfpURL sql.NullString - var bio sql.NullString - var createdAtString sql.NullString - var updatedAtString sql.NullString - err := row.Scan(&user.ID, &user.Username, &displayName, &pfpURL, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions) - if err == sql.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, err - } - if displayName.Valid { - user.DisplayName = displayName.String - } else { - user.DisplayName = "" - } - if pfpURL.Valid { - user.PfpURL = pfpURL.String - } else { - user.PfpURL = "" - } - if bio.Valid { - user.Bio = bio.String - } else { - user.Bio = "" - } - if createdAtString.Valid { - user.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtString.String) - if err != nil { - return nil, fmt.Errorf("error parsing created_at: %v", err) - } - } else { - user.CreatedAt = time.Time{} - } - if updatedAtString.Valid { - user.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtString.String) - if err != nil { - return nil, fmt.Errorf("error parsing updated_at: %v", err) - } - } else { - user.UpdatedAt = time.Time{} - } - return user, nil + 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 = ?" + row := db.QueryRow(query, id) + user := &User{} + var displayName sql.NullString + var bio sql.NullString + var createdAtString sql.NullString + var updatedAtString 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) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + if displayName.Valid { + user.DisplayName = displayName.String + } else { + user.DisplayName = "" + } + if bio.Valid { + user.Bio = bio.String + } else { + user.Bio = "" + } + if createdAtString.Valid { + user.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtString.String) + if err != nil { + return nil, fmt.Errorf("error parsing created_at: %v", err) + } + } else { + user.CreatedAt = time.Time{} + } + if updatedAtString.Valid { + user.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtString.String) + 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) { - 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) - user := &User{} - var displayName sql.NullString - var pfpURL sql.NullString - var bio sql.NullString - var createdAtString sql.NullString - var updatedAtString sql.NullString - err := row.Scan(&user.ID, &user.Username, &displayName, &pfpURL, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions) - if err != nil { - return nil, err - } - if displayName.Valid { - user.DisplayName = displayName.String - } else { - user.DisplayName = "" - } - if pfpURL.Valid { - user.PfpURL = pfpURL.String - } else { - user.PfpURL = "" - } - if bio.Valid { - user.Bio = bio.String - } else { - user.Bio = "" - } - if createdAtString.Valid { - user.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtString.String) - if err != nil { - return nil, fmt.Errorf("error parsing created_at: %v", err) - } - } else { - user.CreatedAt = time.Time{} - } - if updatedAtString.Valid { - user.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtString.String) - if err != nil { - return nil, fmt.Errorf("error parsing updated_at: %v", err) - } - } else { - user.UpdatedAt = time.Time{} - } - return user, nil + 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 = ?" + row := db.QueryRow(query, username) + user := &User{} + var displayName sql.NullString + var bio sql.NullString + var createdAtString sql.NullString + var updatedAtString 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) + if err != nil { + return nil, err + } + if displayName.Valid { + user.DisplayName = displayName.String + } else { + user.DisplayName = "" + } + if bio.Valid { + user.Bio = bio.String + } else { + user.Bio = "" + } + if createdAtString.Valid { + user.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtString.String) + if err != nil { + return nil, fmt.Errorf("error parsing created_at: %v", err) + } + } else { + user.CreatedAt = time.Time{} + } + if updatedAtString.Valid { + user.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtString.String) + 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 { - if algorithm != "sha256" { - return false - } - computedHash := HashPassword(password, salt, algorithm) - return computedHash == hash + if algorithm != "sha256" { + return false + } + computedHash := HashPassword(password, salt, algorithm) + return computedHash == hash } func HashPassword(password, salt, algorithm string) string { - if algorithm != "sha256" { - return "" - } - data := password + salt - hash := sha256.Sum256([]byte(data)) - return fmt.Sprintf("%x", hash) + if algorithm != "sha256" { + return "" + } + data := password + salt + hash := sha256.Sum256([]byte(data)) + return fmt.Sprintf("%x", hash) } func CreateUser(db *sql.DB, username, password string) error { - salt := "random-salt" // Replace with secure random generation - algorithm := "sha256" - 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)" - _, err := db.Exec(query, username, hash, salt, algorithm, false) - return err + salt := "random-salt" // Replace with secure random generation + algorithm := "sha256" + 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)" + _, err := db.Exec(query, username, hash, salt, algorithm, false) + return err } -func UpdateUserProfile(db *sql.DB, userID int, displayName, pfpURL, bio string) error { - query := "UPDATE users SET display_name = ?, pfp_url = ?, bio = ?, updated_at = NOW() WHERE id = ?" - _, err := db.Exec(query, displayName, pfpURL, bio, userID) - return err +func UpdateUserProfile(db *sql.DB, userID int, displayName, bio string) error { + query := "UPDATE users SET display_name = ?, bio = ?, updated_at = NOW() WHERE id = ?" + _, err := db.Exec(query, displayName, bio, userID) + 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 ( - PermCreateBoard int64 = 1 << 0 - PermManageUsers int64 = 1 << 1 + PermCreateBoard int64 = 1 << 0 + PermManageUsers int64 = 1 << 1 ) func HasGlobalPermission(user *User, perm int64) bool { - return user.Permissions&perm != 0 -} \ No newline at end of file + return user.Permissions&perm != 0 +} diff --git a/templates/pages/chat.html b/templates/pages/chat.html index 494de47..93e5d9d 100644 --- a/templates/pages/chat.html +++ b/templates/pages/chat.html @@ -224,8 +224,8 @@ {{range .Messages}}
- {{if .PfpURL}} - PFP + {{if .PfpFileID.Valid}} + PFP {{else}}
{{end}} @@ -262,7 +262,7 @@ let replyUsername = ''; function connectWebSocket() { - ws = new WebSocket('ws://' + window.location.host + '{{.BasePath}}/chat/?ws=true', [], { credentials: 'include' }); + ws = new WebSocket('ws://' + window.location.host + '{{.BasePath}}/chat/?ws=true'); ws.onmessage = function(event) { const msg = JSON.parse(event.data); appendMessage(msg); @@ -298,21 +298,26 @@ const messages = document.getElementById('chat-messages'); const msgDiv = document.createElement('div'); msgDiv.className = 'chat-message'; - msgDiv.id = 'msg-' + msg.ID; - let pfpHTML = msg.PfpURL ? `PFP` : `
`; - let replyHTML = msg.ReplyTo > 0 ? `
Replying to ${msg.Username}
` : ''; + msgDiv.id = 'msg-' + msg.id; + let pfpHTML = ''; + if (msg.pfpFileId && msg.pfpFileId.Valid) { + pfpHTML = `PFP`; + } else { + pfpHTML = `
`; + } + let replyHTML = msg.replyTo > 0 ? `
Replying to ${msg.username}
` : ''; // Process content for mentions - let content = msg.Content.replace(/@[\w]+/g, match => `${match}`); + let content = msg.content.replace(/@[\w]+/g, match => `${match}`); msgDiv.innerHTML = `
${pfpHTML} - ${msg.Username} - ${new Date(msg.Timestamp).toLocaleString()} + ${msg.username} + ${new Date(msg.timestamp).toLocaleString()}
${replyHTML}
${content}
- Reply + Reply
`; messages.appendChild(msgDiv); @@ -382,6 +387,7 @@ if (atIndex !== -1 && (caretPos === text.length || text[caretPos] === ' ')) { const prefix = text.substring(atIndex + 1, caretPos); autocompletePrefix = prefix; + // TODO: Fix this. const response = await fetch('{{.BasePath}}/chat/?autocomplete=true&prefix=' + encodeURIComponent(prefix)); const usernames = await response.json(); if (usernames.length > 0) { @@ -432,4 +438,4 @@ -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/pages/profile.html b/templates/pages/profile.html index 67ca93a..53653b3 100644 --- a/templates/pages/profile.html +++ b/templates/pages/profile.html @@ -14,8 +14,8 @@

Username: {{.User.Username}}

Display Name: {{.DisplayName}}

- {{if .User.PfpURL}} - Profile Picture + {{if .User.PfpFileID.Valid}} + Profile Picture {{end}}

Bio: {{.User.Bio}}

Joined: {{.User.CreatedAt}}

@@ -27,4 +27,4 @@ {{template "cookie_banner" .}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/pages/profile_edit.html b/templates/pages/profile_edit.html index 80d01f5..249e2df 100644 --- a/templates/pages/profile_edit.html +++ b/templates/pages/profile_edit.html @@ -12,11 +12,11 @@

Edit Profile

-
+
- -
+ +

@@ -26,4 +26,4 @@ {{template "cookie_banner" .}} -{{end}} \ No newline at end of file +{{end}}