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:
@@ -0,0 +1,120 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
|
||||
"github.com/google/uuid"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
client *minio.Client
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewHandler(cfg *config.Config) (*Handler, error) {
|
||||
endpoint := cfg.MinIO.Endpoint
|
||||
if endpoint == "" {
|
||||
return nil, fmt.Errorf("minio endpoint not configured")
|
||||
}
|
||||
|
||||
client, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(cfg.MinIO.AccessKey, cfg.MinIO.SecretKey, ""),
|
||||
Secure: cfg.MinIO.UseSSL,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minio client: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
exists, err := client.BucketExists(ctx, cfg.MinIO.Bucket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bucket check: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
if err := client.MakeBucket(ctx, cfg.MinIO.Bucket, minio.MakeBucketOptions{}); err != nil {
|
||||
return nil, fmt.Errorf("make bucket: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
client: client,
|
||||
bucket: cfg.MinIO.Bucket,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Handler) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(50 << 20); err != nil {
|
||||
http.Error(w, `{"error":"invalid form"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"missing file"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
ext := ""
|
||||
if idx := strings.LastIndex(header.Filename, "."); idx >= 0 {
|
||||
ext = header.Filename[idx:]
|
||||
}
|
||||
|
||||
objectName := uuid.New().String() + ext
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
info, err := h.client.PutObject(ctx, h.bucket, objectName, file, header.Size, minio.PutObjectOptions{
|
||||
ContentType: contentType,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"upload failed"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"url":"/%s/%s","size":%d}`, h.bucket, objectName, info.Size)
|
||||
}
|
||||
|
||||
func (h *Handler) Serve(w http.ResponseWriter, r *http.Request) {
|
||||
objectName := strings.TrimPrefix(r.URL.Path, "/files/")
|
||||
if objectName == "" {
|
||||
http.Error(w, `{"error":"missing file"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
obj, err := h.client.GetObject(ctx, h.bucket, objectName, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
stat, err := obj.Stat()
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", stat.ContentType)
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
|
||||
io.Copy(w, obj)
|
||||
}
|
||||
Reference in New Issue
Block a user