685 lines
18 KiB
Go
685 lines
18 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"app_compartir/internal/auth"
|
|
"app_compartir/internal/config"
|
|
"app_compartir/internal/db"
|
|
"app_compartir/internal/models"
|
|
"app_compartir/internal/storage"
|
|
"github.com/skip2/go-qrcode"
|
|
)
|
|
|
|
type Handlers struct {
|
|
cfg *config.Config
|
|
db *db.Database
|
|
auth *auth.AuthService
|
|
templates map[string]*template.Template
|
|
}
|
|
|
|
func New(cfg *config.Config, database *db.Database, authSvc *auth.AuthService) (*Handlers, error) {
|
|
h := &Handlers{
|
|
cfg: cfg,
|
|
db: database,
|
|
auth: authSvc,
|
|
templates: make(map[string]*template.Template),
|
|
}
|
|
|
|
if err := h.parseTemplates(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return h, nil
|
|
}
|
|
|
|
func (h *Handlers) parseTemplates() error {
|
|
pages := []string{"index.html", "viewer.html", "login.html", "my_shares.html", "expired.html"}
|
|
basePath := "web/templates/base.html"
|
|
|
|
for _, page := range pages {
|
|
pagePath := filepath.Join("web/templates", page)
|
|
tmpl, err := template.ParseFiles(basePath, pagePath)
|
|
if err != nil {
|
|
return fmt.Errorf("error parseando plantilla %s: %w", page, err)
|
|
}
|
|
h.templates[page] = tmpl
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type PageData struct {
|
|
Session *models.Session
|
|
Share *models.Share
|
|
Shares []*models.Share
|
|
FormattedDate string
|
|
IsImage bool
|
|
IsVideo bool
|
|
IsAudio bool
|
|
IsPDF bool
|
|
IsText bool
|
|
HasGoogle bool
|
|
Error string
|
|
}
|
|
|
|
func (h *Handlers) getBaseURL(r *http.Request) string {
|
|
proto := "http"
|
|
if r.TLS != nil ||
|
|
strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") ||
|
|
strings.EqualFold(r.Header.Get("X-Forwarded-Ssl"), "on") ||
|
|
r.Header.Get("X-Forwarded-Port") == "443" {
|
|
proto = "https"
|
|
}
|
|
host := r.Host
|
|
if host == "" {
|
|
host = r.Header.Get("X-Forwarded-Host")
|
|
}
|
|
if host != "" {
|
|
if strings.HasPrefix(h.cfg.BaseURL, "https://") && strings.Contains(h.cfg.BaseURL, host) {
|
|
proto = "https"
|
|
}
|
|
return fmt.Sprintf("%s://%s", proto, host)
|
|
}
|
|
return h.cfg.BaseURL
|
|
}
|
|
|
|
func (h *Handlers) Index(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
session := h.auth.GetOrCreateSession(w, r)
|
|
data := PageData{
|
|
Session: session,
|
|
}
|
|
|
|
tmpl := h.templates["index.html"]
|
|
if err := tmpl.ExecuteTemplate(w, "base.html", data); err != nil {
|
|
log.Printf("Error renderizando index.html: %v", err)
|
|
http.Error(w, "Error interno", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *Handlers) Upload(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Método no permitido", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Límite de 500 MB por subida
|
|
r.Body = http.MaxBytesReader(w, r.Body, 500<<20)
|
|
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
|
http.Error(w, "Archivo demasiado grande o formulario inválido", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
file, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
http.Error(w, "No se encontró el archivo en la petición", http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
session := h.auth.GetOrCreateSession(w, r)
|
|
slug := storage.GenerateSlug(7)
|
|
|
|
storedPath, written, err := storage.SaveFile(h.cfg.UploadDir, slug, header.Filename, file)
|
|
if err != nil {
|
|
log.Printf("Error guardando archivo en disco: %v", err)
|
|
http.Error(w, "Error guardando archivo en el servidor", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Detectar tipo MIME
|
|
mimeType := storage.DetectMimeType(header.Filename, nil)
|
|
now := time.Now()
|
|
|
|
var expiresAt *time.Time
|
|
var userID *string
|
|
userDisplay := "Invitado"
|
|
|
|
if session.IsGuest {
|
|
// REGLA FUNDAMENTAL: Solo los invitados expiran en 24 horas
|
|
exp := now.Add(24 * time.Hour)
|
|
expiresAt = &exp
|
|
} else if session.User != nil {
|
|
// Las cuentas registradas son PERMANENTES
|
|
expiresAt = nil
|
|
userID = &session.User.ID
|
|
userDisplay = session.User.Name
|
|
if userDisplay == "" {
|
|
userDisplay = session.User.Email
|
|
}
|
|
}
|
|
|
|
share := &models.Share{
|
|
ID: slug,
|
|
UserID: userID,
|
|
UserDisplay: userDisplay,
|
|
IsGuest: session.IsGuest,
|
|
Type: "file",
|
|
Title: header.Filename,
|
|
StoredPath: storedPath,
|
|
FileSize: written,
|
|
MimeType: mimeType,
|
|
TextContent: "",
|
|
ViewsCount: 0,
|
|
DownloadsCount: 0,
|
|
CreatedAt: now,
|
|
ExpiresAt: expiresAt,
|
|
}
|
|
|
|
if err := h.db.CreateShare(share); err != nil {
|
|
log.Printf("Error registrando share en BD: %v", err)
|
|
_ = os.Remove(filepath.Join(h.cfg.UploadDir, storedPath))
|
|
http.Error(w, "Error registrando archivo en base de datos", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
baseURL := h.getBaseURL(r)
|
|
resp := map[string]interface{}{
|
|
"id": share.ID,
|
|
"title": share.Title,
|
|
"file_size": share.FileSize,
|
|
"mime_type": share.MimeType,
|
|
"is_guest": share.IsGuest,
|
|
"viewer_url": fmt.Sprintf("%s/v/%s", baseURL, share.ID),
|
|
"download_url": fmt.Sprintf("%s/d/%s", baseURL, share.ID),
|
|
}
|
|
if share.IsGuest {
|
|
resp["expires_in"] = "24 horas"
|
|
} else {
|
|
resp["expires_in"] = "Permanente"
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func (h *Handlers) ShareText(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Método no permitido", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Límite de 5 MB para evitar DoS por memoria
|
|
r.Body = http.MaxBytesReader(w, r.Body, 5<<20)
|
|
|
|
var req struct {
|
|
Title string `json:"title"`
|
|
Text string `json:"text"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Text) == "" {
|
|
http.Error(w, "Contenido de texto requerido o tamaño excesivo (máx 5 MB)", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
session := h.auth.GetOrCreateSession(w, r)
|
|
slug := storage.GenerateSlug(7)
|
|
now := time.Now()
|
|
|
|
title := strings.TrimSpace(req.Title)
|
|
if title == "" {
|
|
// Generar título a partir de la primera línea de texto
|
|
lines := strings.Split(req.Text, "\n")
|
|
firstLine := strings.TrimSpace(lines[0])
|
|
if len(firstLine) > 40 {
|
|
firstLine = firstLine[:40] + "..."
|
|
}
|
|
if firstLine != "" {
|
|
title = firstLine
|
|
} else {
|
|
title = "Nota de texto"
|
|
}
|
|
}
|
|
|
|
var expiresAt *time.Time
|
|
var userID *string
|
|
userDisplay := "Invitado"
|
|
|
|
if session.IsGuest {
|
|
// REGLA: Los invitados expiran a las 24 horas
|
|
exp := now.Add(24 * time.Hour)
|
|
expiresAt = &exp
|
|
} else if session.User != nil {
|
|
// Los registrados son PERMANENTES
|
|
expiresAt = nil
|
|
userID = &session.User.ID
|
|
userDisplay = session.User.Name
|
|
if userDisplay == "" {
|
|
userDisplay = session.User.Email
|
|
}
|
|
}
|
|
|
|
share := &models.Share{
|
|
ID: slug,
|
|
UserID: userID,
|
|
UserDisplay: userDisplay,
|
|
IsGuest: session.IsGuest,
|
|
Type: "text",
|
|
Title: title,
|
|
StoredPath: "",
|
|
FileSize: int64(len(req.Text)),
|
|
MimeType: "text/plain; charset=utf-8",
|
|
TextContent: req.Text,
|
|
ViewsCount: 0,
|
|
DownloadsCount: 0,
|
|
CreatedAt: now,
|
|
ExpiresAt: expiresAt,
|
|
}
|
|
|
|
if err := h.db.CreateShare(share); err != nil {
|
|
log.Printf("Error registrando share de texto en BD: %v", err)
|
|
http.Error(w, "Error guardando texto en base de datos", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
baseURL := h.getBaseURL(r)
|
|
resp := map[string]interface{}{
|
|
"id": share.ID,
|
|
"title": share.Title,
|
|
"file_size": share.FileSize,
|
|
"mime_type": share.MimeType,
|
|
"is_guest": share.IsGuest,
|
|
"viewer_url": fmt.Sprintf("%s/v/%s", baseURL, share.ID),
|
|
"download_url": fmt.Sprintf("%s/d/%s", baseURL, share.ID),
|
|
}
|
|
if share.IsGuest {
|
|
resp["expires_in"] = "24 horas"
|
|
} else {
|
|
resp["expires_in"] = "Permanente"
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func (h *Handlers) View(w http.ResponseWriter, r *http.Request) {
|
|
id := strings.TrimPrefix(r.URL.Path, "/v/")
|
|
if id == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
share, err := h.db.GetShare(id)
|
|
if err != nil || share == nil {
|
|
h.renderExpired(w, r)
|
|
return
|
|
}
|
|
|
|
// Verificar si ya expiró (en caso de que el worker aún no lo haya borrado)
|
|
if share.IsGuest && share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) {
|
|
h.renderExpired(w, r)
|
|
return
|
|
}
|
|
|
|
_ = h.db.IncrementViews(id)
|
|
share.ViewsCount++
|
|
|
|
// Si es un archivo de texto/código y no tiene TextContent, leer hasta 512 KB para vista previa segura
|
|
isText := strings.HasPrefix(share.MimeType, "text/") || share.Type == "text"
|
|
if isText && share.TextContent == "" && share.StoredPath != "" {
|
|
fullPath := filepath.Join(h.cfg.UploadDir, share.StoredPath)
|
|
if fi, err := os.Stat(fullPath); err == nil && fi.Size() <= 512*1024 {
|
|
if content, err := os.ReadFile(fullPath); err == nil {
|
|
share.TextContent = string(content)
|
|
}
|
|
}
|
|
}
|
|
|
|
session := h.auth.GetOrCreateSession(w, r)
|
|
data := PageData{
|
|
Session: session,
|
|
Share: share,
|
|
FormattedDate: share.CreatedAt.Format("02/01/2006 15:04"),
|
|
IsImage: strings.HasPrefix(share.MimeType, "image/"),
|
|
IsVideo: strings.HasPrefix(share.MimeType, "video/"),
|
|
IsAudio: strings.HasPrefix(share.MimeType, "audio/"),
|
|
IsPDF: share.MimeType == "application/pdf",
|
|
IsText: isText,
|
|
}
|
|
|
|
tmpl := h.templates["viewer.html"]
|
|
if err := tmpl.ExecuteTemplate(w, "base.html", data); err != nil {
|
|
log.Printf("Error renderizando visor: %v", err)
|
|
http.Error(w, "Error interno", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *Handlers) renderExpired(w http.ResponseWriter, r *http.Request) {
|
|
session := h.auth.GetOrCreateSession(w, r)
|
|
data := PageData{
|
|
Session: session,
|
|
}
|
|
tmpl := h.templates["expired.html"]
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_ = tmpl.ExecuteTemplate(w, "base.html", data)
|
|
}
|
|
|
|
func isSafeInlineMime(mimeType string) bool {
|
|
switch mimeType {
|
|
case "image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp", "image/x-icon":
|
|
return true
|
|
case "audio/mpeg", "audio/wav", "audio/ogg", "audio/webm", "audio/aac", "audio/flac":
|
|
return true
|
|
case "video/mp4", "video/webm", "video/ogg":
|
|
return true
|
|
case "application/pdf":
|
|
return true
|
|
case "text/plain", "text/plain; charset=utf-8":
|
|
return true
|
|
case "image/svg+xml":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func sanitizeHeaderFilename(name string) string {
|
|
cleaned := strings.ReplaceAll(name, "\r", "")
|
|
cleaned = strings.ReplaceAll(cleaned, "\n", "")
|
|
cleaned = strings.ReplaceAll(cleaned, "\"", "_")
|
|
if strings.TrimSpace(cleaned) == "" {
|
|
cleaned = "archivo.bin"
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
func (h *Handlers) Download(w http.ResponseWriter, r *http.Request) {
|
|
id := strings.TrimPrefix(r.URL.Path, "/d/")
|
|
if id == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
share, err := h.db.GetShare(id)
|
|
if err != nil || share == nil {
|
|
h.renderExpired(w, r)
|
|
return
|
|
}
|
|
|
|
if share.IsGuest && share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) {
|
|
h.renderExpired(w, r)
|
|
return
|
|
}
|
|
|
|
_ = h.db.IncrementDownloads(id)
|
|
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
safeTitle := sanitizeHeaderFilename(share.Title)
|
|
|
|
if share.Type == "text" {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", safeTitle+".txt"))
|
|
_, _ = w.Write([]byte(share.TextContent))
|
|
return
|
|
}
|
|
|
|
fullPath := filepath.Join(h.cfg.UploadDir, share.StoredPath)
|
|
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
|
h.renderExpired(w, r)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", safeTitle))
|
|
http.ServeFile(w, r, fullPath)
|
|
}
|
|
|
|
func (h *Handlers) Raw(w http.ResponseWriter, r *http.Request) {
|
|
id := strings.TrimPrefix(r.URL.Path, "/raw/")
|
|
if id == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
share, err := h.db.GetShare(id)
|
|
if err != nil || share == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
if share.IsGuest && share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
// Cabeceras de seguridad estrictas contra XSS y MIME-sniffing
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
|
|
|
if share.Type == "text" {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", "inline")
|
|
_, _ = w.Write([]byte(share.TextContent))
|
|
return
|
|
}
|
|
|
|
fullPath := filepath.Join(h.cfg.UploadDir, share.StoredPath)
|
|
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
// Neutralizar cualquier intento de servir HTML, PHP o scripts ejecutables inline
|
|
mimeType := share.MimeType
|
|
if strings.HasPrefix(mimeType, "text/html") || strings.Contains(mimeType, "php") || strings.Contains(mimeType, "javascript") {
|
|
mimeType = "text/plain; charset=utf-8"
|
|
}
|
|
w.Header().Set("Content-Type", mimeType)
|
|
|
|
if isSafeInlineMime(mimeType) {
|
|
w.Header().Set("Content-Disposition", "inline")
|
|
} else {
|
|
// Archivos ejecutables, binarios o desconocidos se fuerzan a descarga
|
|
safeTitle := sanitizeHeaderFilename(share.Title)
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", safeTitle))
|
|
}
|
|
|
|
http.ServeFile(w, r, fullPath)
|
|
}
|
|
|
|
func (h *Handlers) QR(w http.ResponseWriter, r *http.Request) {
|
|
id := strings.TrimPrefix(r.URL.Path, "/qr/")
|
|
if id == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
baseURL := h.getBaseURL(r)
|
|
targetURL := fmt.Sprintf("%s/v/%s", baseURL, id)
|
|
pngBytes, err := qrcode.Encode(targetURL, qrcode.Medium, 256)
|
|
if err != nil {
|
|
http.Error(w, "Error generando QR", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
w.Header().Set("Cache-Control", "public, max-age=86400")
|
|
_, _ = w.Write(pngBytes)
|
|
}
|
|
|
|
func (h *Handlers) LoginView(w http.ResponseWriter, r *http.Request) {
|
|
session := h.auth.GetOrCreateSession(w, r)
|
|
if !session.IsGuest && session.User != nil {
|
|
http.Redirect(w, r, "/mis-archivos", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
data := PageData{
|
|
Session: session,
|
|
HasGoogle: h.auth.HasGoogleConfigured(),
|
|
}
|
|
|
|
tmpl := h.templates["login.html"]
|
|
_ = tmpl.ExecuteTemplate(w, "base.html", data)
|
|
}
|
|
|
|
func (h *Handlers) WebControlLogin(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
email := strings.TrimSpace(r.FormValue("email"))
|
|
password := strings.TrimSpace(r.FormValue("password"))
|
|
|
|
if email == "" || password == "" {
|
|
h.renderLoginError(w, r, "Por favor ingresa usuario y contraseña.")
|
|
return
|
|
}
|
|
|
|
user, err := h.auth.LoginWebControl(email, password)
|
|
if err != nil {
|
|
log.Printf("Fallo login WebControl: %v", err)
|
|
h.renderLoginError(w, r, "Credenciales incorrectas o servidor WebControl no disponible.")
|
|
return
|
|
}
|
|
|
|
if err := h.auth.AttachUserToSession(w, r, user); err != nil {
|
|
log.Printf("Error adjuntando sesión: %v", err)
|
|
h.renderLoginError(w, r, "Error interno estableciendo sesión.")
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/mis-archivos", http.StatusFound)
|
|
}
|
|
|
|
func (h *Handlers) renderLoginError(w http.ResponseWriter, r *http.Request, msg string) {
|
|
session := h.auth.GetOrCreateSession(w, r)
|
|
data := PageData{
|
|
Session: session,
|
|
HasGoogle: h.auth.HasGoogleConfigured(),
|
|
Error: msg,
|
|
}
|
|
tmpl := h.templates["login.html"]
|
|
_ = tmpl.ExecuteTemplate(w, "base.html", data)
|
|
}
|
|
|
|
func (h *Handlers) GoogleLogin(w http.ResponseWriter, r *http.Request) {
|
|
state := h.auth.GenerateToken(16)
|
|
// Guardar state en cookie temporal
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "oauth_state",
|
|
Value: state,
|
|
Path: "/",
|
|
Expires: time.Now().Add(10 * time.Minute),
|
|
HttpOnly: true,
|
|
})
|
|
|
|
url, err := h.auth.GetGoogleAuthURL(state)
|
|
if err != nil {
|
|
h.renderLoginError(w, r, err.Error())
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
func (h *Handlers) GoogleCallback(w http.ResponseWriter, r *http.Request) {
|
|
stateCookie, err := r.Cookie("oauth_state")
|
|
if err != nil || stateCookie.Value == "" || stateCookie.Value != r.URL.Query().Get("state") {
|
|
h.renderLoginError(w, r, "Error de verificación CSRF en login con Google.")
|
|
return
|
|
}
|
|
|
|
code := r.URL.Query().Get("code")
|
|
if code == "" {
|
|
h.renderLoginError(w, r, "No se recibió código de autenticación de Google.")
|
|
return
|
|
}
|
|
|
|
user, err := h.auth.HandleGoogleCallback(r.Context(), code)
|
|
if err != nil {
|
|
log.Printf("Error callback Google: %v", err)
|
|
h.renderLoginError(w, r, "No se pudo completar el inicio de sesión con Google.")
|
|
return
|
|
}
|
|
|
|
if err := h.auth.AttachUserToSession(w, r, user); err != nil {
|
|
h.renderLoginError(w, r, "Error interno estableciendo sesión.")
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/mis-archivos", http.StatusFound)
|
|
}
|
|
|
|
func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request) {
|
|
h.auth.Logout(w, r)
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
}
|
|
|
|
func (h *Handlers) MyShares(w http.ResponseWriter, r *http.Request) {
|
|
session := h.auth.GetOrCreateSession(w, r)
|
|
if session.IsGuest || session.User == nil {
|
|
http.Redirect(w, r, "/login", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
shares, err := h.db.GetUserShares(session.User.ID)
|
|
if err != nil {
|
|
log.Printf("Error obteniendo archivos de usuario: %v", err)
|
|
shares = []*models.Share{}
|
|
}
|
|
|
|
data := PageData{
|
|
Session: session,
|
|
Shares: shares,
|
|
}
|
|
|
|
tmpl := h.templates["my_shares.html"]
|
|
_ = tmpl.ExecuteTemplate(w, "base.html", data)
|
|
}
|
|
|
|
func (h *Handlers) DeleteShare(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodDelete {
|
|
http.Error(w, "Método no permitido", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
id := strings.TrimPrefix(r.URL.Path, "/api/shares/")
|
|
if id == "" {
|
|
http.Error(w, "ID requerido", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
session := h.auth.GetOrCreateSession(w, r)
|
|
if session.IsGuest || session.User == nil {
|
|
http.Error(w, "No autorizado", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
share, err := h.db.GetShare(id)
|
|
if err != nil || share == nil {
|
|
http.Error(w, "Archivo no encontrado", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Verificar pertenencia
|
|
if share.UserID == nil || *share.UserID != session.User.ID {
|
|
http.Error(w, "No tienes permiso para eliminar este archivo", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
if share.StoredPath != "" {
|
|
_ = os.Remove(filepath.Join(h.cfg.UploadDir, share.StoredPath))
|
|
}
|
|
|
|
if err := h.db.DeleteShare(id, &session.User.ID); err != nil {
|
|
http.Error(w, "Error eliminando de base de datos", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]bool{"ok": true})
|
|
}
|