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.jocadbz
parent
bdf81e7c68
commit
7b0528ef36
|
@ -1,5 +1,8 @@
|
|||
config/config.json
|
||||
config/about_page.htmlbody
|
||||
|
||||
# Testing
|
||||
files/
|
||||
|
||||
# nano
|
||||
.swp
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
64
main.go
64
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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
258
models/user.go
258
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
|
||||
}
|
||||
return user.Permissions&perm != 0
|
||||
}
|
||||
|
|
|
@ -224,8 +224,8 @@
|
|||
{{range .Messages}}
|
||||
<div class="chat-message" id="msg-{{.ID}}">
|
||||
<div class="chat-message-header">
|
||||
{{if .PfpURL}}
|
||||
<img src="{{.PfpURL}}" alt="PFP" class="chat-message-pfp">
|
||||
{{if .PfpFileID.Valid}}
|
||||
<img src="{{$.BasePath}}/file?id={{.PfpFileID.Int64}}" alt="PFP" class="chat-message-pfp">
|
||||
{{else}}
|
||||
<div class="chat-message-pfp" style="background-color: #001858;"></div>
|
||||
{{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 ? `<img src="${msg.PfpURL}" alt="PFP" class="chat-message-pfp">` : `<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>` : '';
|
||||
msgDiv.id = 'msg-' + msg.id;
|
||||
let pfpHTML = '';
|
||||
if (msg.pfpFileId && msg.pfpFileId.Valid) {
|
||||
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
|
||||
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 = `
|
||||
<div class="chat-message-header">
|
||||
${pfpHTML}
|
||||
<span class="chat-message-username">${msg.Username}</span>
|
||||
<span class="chat-message-timestamp">${new Date(msg.Timestamp).toLocaleString()}</span>
|
||||
<span class="chat-message-username">${msg.username}</span>
|
||||
<span class="chat-message-timestamp">${new Date(msg.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
${replyHTML}
|
||||
<div class="chat-message-content">${content}</div>
|
||||
<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>
|
||||
`;
|
||||
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 @@
|
|||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
<section>
|
||||
<p>Username: {{.User.Username}}</p>
|
||||
<p>Display Name: {{.DisplayName}}</p>
|
||||
{{if .User.PfpURL}}
|
||||
<img src="{{.User.PfpURL}}" alt="Profile Picture">
|
||||
{{if .User.PfpFileID.Valid}}
|
||||
<img src="{{.BasePath}}/file?id={{.User.PfpFileID.Int64}}" alt="Profile Picture">
|
||||
{{end}}
|
||||
<p>Bio: {{.User.Bio}}</p>
|
||||
<p>Joined: {{.User.CreatedAt}}</p>
|
||||
|
@ -27,4 +27,4 @@
|
|||
{{template "cookie_banner" .}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
@ -12,11 +12,11 @@
|
|||
<h2>Edit Profile</h2>
|
||||
</header>
|
||||
<section>
|
||||
<form method="post" action="{{.BasePath}}/profile/edit/">
|
||||
<form method="post" action="{{.BasePath}}/profile/edit/" enctype="multipart/form-data">
|
||||
<label for="display_name">Display Name:</label>
|
||||
<input type="text" id="display_name" name="display_name" value="{{.User.DisplayName}}"><br>
|
||||
<label for="pfp_url">Profile Picture URL:</label>
|
||||
<input type="text" id="pfp_url" name="pfp_url" value="{{.User.PfpURL}}"><br>
|
||||
<label for="pfp">Profile Picture:</label>
|
||||
<input type="file" id="pfp" name="pfp"><br>
|
||||
<label for="bio">Bio:</label>
|
||||
<textarea id="bio" name="bio">{{.User.Bio}}</textarea><br>
|
||||
<input type="submit" value="Save">
|
||||
|
@ -26,4 +26,4 @@
|
|||
{{template "cookie_banner" .}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in New Issue