Profiles, Giphy, uploads, settings UI
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
This commit is contained in:
@@ -54,10 +54,15 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
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,
|
||||
|
||||
+57
-2
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -11,9 +12,11 @@ import (
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/db"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/giphy"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/upload"
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimw "github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
@@ -42,6 +45,18 @@ func main() {
|
||||
hub := gateway.NewHub(logger)
|
||||
go hub.Run()
|
||||
|
||||
// Giphy client (nil if no API key)
|
||||
giphyClient := giphy.NewClient(cfg.Giphy.APIKey)
|
||||
|
||||
// Upload handler (nil if no MinIO config)
|
||||
uploadHandler, err := upload.NewHandler(cfg)
|
||||
if err != nil {
|
||||
logger.Warn("upload handler not available", "error", err)
|
||||
}
|
||||
|
||||
// Auth handler
|
||||
authHandler := auth.NewHandler(database.DB, cfg)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(chimw.Logger)
|
||||
r.Use(chimw.Recoverer)
|
||||
@@ -60,9 +75,9 @@ func main() {
|
||||
|
||||
// API routes
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
// Auth (public)
|
||||
// Auth (public: register, login, logout)
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
auth.NewHandler(database.DB, cfg).RegisterRoutes(r)
|
||||
authHandler.RegisterPublicRoutes(r)
|
||||
})
|
||||
|
||||
// Protected routes
|
||||
@@ -70,6 +85,9 @@ func main() {
|
||||
r.Use(middleware.Session(sessionStore, cfg))
|
||||
r.Use(middleware.RequireAuth)
|
||||
|
||||
// Auth (protected: me, update profile)
|
||||
authHandler.RegisterProtectedRoutes(r)
|
||||
|
||||
// Servers
|
||||
r.Route("/servers", func(r chi.Router) {
|
||||
server.NewHandler(database.DB).RegisterRoutes(r)
|
||||
@@ -84,9 +102,46 @@ func main() {
|
||||
r.Route("/channels/{channelID}/messages", func(r chi.Router) {
|
||||
message.NewHandler(database.DB, hub).RegisterRoutes(r)
|
||||
})
|
||||
|
||||
// Giphy search
|
||||
if giphyClient != nil {
|
||||
r.Get("/gifs/search", func(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
if query == "" {
|
||||
http.Error(w, `{"error":"missing query"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
gifs, err := giphyClient.Search(query, 20)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"search failed"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(gifs)
|
||||
})
|
||||
r.Get("/gifs/trending", func(w http.ResponseWriter, r *http.Request) {
|
||||
gifs, err := giphyClient.GetTrending(20)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"trending failed"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(gifs)
|
||||
})
|
||||
}
|
||||
|
||||
// File upload
|
||||
if uploadHandler != nil {
|
||||
r.Post("/upload", uploadHandler.Upload)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// File serving (public, for viewing uploaded files)
|
||||
if uploadHandler != nil {
|
||||
r.Get("/files/*", uploadHandler.Serve)
|
||||
}
|
||||
|
||||
// Static file serving for production (SPA)
|
||||
staticDir := "/srv/web"
|
||||
if _, err := os.Stat(staticDir); err == nil {
|
||||
|
||||
Reference in New Issue
Block a user