e69553af02
Backend: - Auth: split routes into RegisterPublicRoutes + RegisterProtectedRoutes - Auth: PATCH /me for profile updates (display_name, bio, accent_color, status_text) - Auth: Gravatar fallback for avatars on register - DB: users table now has bio, accent_color, status_text columns - giphy/client.go: Search() and GetTrending() against Giphy API - upload/handlers.go: MinIO file upload + serve - config: added GiphyConfig and MinIO config - cmd/server: wired giphy (/gifs/search, /gifs/trending), upload (/upload), files (/files/*) routes Frontend: - auth store: updated User interface with new profile fields, added updateProfile() - UserSettings.tsx: terminal-styled profile editor (display_name, bio, accent_color, status_text, avatar upload) - GiphyPicker.tsx: terminal-styled GIF picker with search + trending - ChatArea.tsx: integrated [GIF] button into message input - App.tsx: imports UserSettings, added /settings route
151 lines
4.1 KiB
Go
151 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
)
|
|
|
|
func main() {
|
|
direction := flag.String("direction", "up", "migration direction: up or down")
|
|
flag.Parse()
|
|
|
|
cfg := config.Load()
|
|
|
|
db, err := sql.Open("pgx", cfg.DatabaseDSN())
|
|
if err != nil {
|
|
log.Fatalf("failed to open database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := db.Ping(); err != nil {
|
|
log.Fatalf("failed to ping database: %v", err)
|
|
}
|
|
|
|
switch *direction {
|
|
case "up":
|
|
fmt.Println("running migrations up...")
|
|
if err := runUp(db); err != nil {
|
|
log.Fatalf("migration failed: %v", err)
|
|
}
|
|
fmt.Println("migrations complete")
|
|
case "down":
|
|
fmt.Println("running migrations down...")
|
|
if err := runDown(db); err != nil {
|
|
log.Fatalf("migration failed: %v", err)
|
|
}
|
|
fmt.Println("migrations complete")
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown direction: %s\n", *direction)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
const upSQL = `
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
username VARCHAR(32) NOT NULL UNIQUE,
|
|
display_name VARCHAR(32),
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255),
|
|
avatar TEXT,
|
|
bio VARCHAR(250) DEFAULT '',
|
|
accent_color VARCHAR(7) DEFAULT '',
|
|
status VARCHAR(16) NOT NULL DEFAULT 'offline',
|
|
status_text VARCHAR(128) DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token VARCHAR(255) NOT NULL UNIQUE,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS servers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(100) NOT NULL,
|
|
icon TEXT,
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
type VARCHAR(16) NOT NULL DEFAULT 'text',
|
|
category VARCHAR(100) NOT NULL DEFAULT 'general',
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (server_id, name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS members (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (user_id, server_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
content TEXT NOT NULL,
|
|
edited_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS roles (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
color VARCHAR(7),
|
|
permissions BIGINT NOT NULL DEFAULT 0,
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS member_roles (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (user_id, role_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_messages_channel_created ON messages(channel_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
|
|
CREATE INDEX IF NOT EXISTS idx_members_server_user ON members(server_id, user_id);
|
|
`
|
|
|
|
const downSQL = `
|
|
DROP TABLE IF EXISTS member_roles;
|
|
DROP TABLE IF EXISTS roles;
|
|
DROP TABLE IF EXISTS messages;
|
|
DROP TABLE IF EXISTS members;
|
|
DROP TABLE IF EXISTS channels;
|
|
DROP TABLE IF EXISTS servers;
|
|
DROP TABLE IF EXISTS sessions;
|
|
DROP TABLE IF EXISTS users;
|
|
`
|
|
|
|
func runUp(db *sql.DB) error {
|
|
_, err := db.Exec(upSQL)
|
|
return err
|
|
}
|
|
|
|
func runDown(db *sql.DB) error {
|
|
_, err := db.Exec(downSQL)
|
|
return err
|
|
}
|