feat: implementar webapp app_compartir con visor universal, auth y purga 24h para guest

This commit is contained in:
2026-10-13 22:46:02 -05:00
parent 6ea00e4ec5
commit 8519590cc7
24 changed files with 2970 additions and 71 deletions
+329
View File
@@ -0,0 +1,329 @@
package db
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"app_compartir/internal/models"
_ "modernc.org/sqlite"
)
type Database struct {
*sql.DB
}
func Init(dbPath, uploadDir string) (*Database, error) {
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
return nil, fmt.Errorf("error creando directorio de bd: %w", err)
}
if err := os.MkdirAll(uploadDir, 0755); err != nil {
return nil, fmt.Errorf("error creando directorio de uploads: %w", err)
}
conn, err := sql.Open("sqlite", dbPath+"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)")
if err != nil {
return nil, fmt.Errorf("error abriendo sqlite: %w", err)
}
d := &Database{DB: conn}
if err := d.migrate(); err != nil {
return nil, fmt.Errorf("error ejecutando migraciones: %w", err)
}
return d, nil
}
func (d *Database) migrate() error {
schema := `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
provider_id TEXT NOT NULL,
email TEXT,
name TEXT,
avatar_url TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id TEXT,
is_guest BOOLEAN NOT NULL DEFAULT 1,
guest_id TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS shares (
id TEXT PRIMARY KEY,
user_id TEXT,
user_display TEXT NOT NULL DEFAULT 'Invitado',
is_guest BOOLEAN NOT NULL DEFAULT 1,
type TEXT NOT NULL DEFAULT 'file',
title TEXT NOT NULL,
stored_path TEXT,
file_size INTEGER NOT NULL DEFAULT 0,
mime_type TEXT NOT NULL DEFAULT 'text/plain',
text_content TEXT,
views_count INTEGER NOT NULL DEFAULT 0,
downloads_count INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_shares_expired_guest ON shares(is_guest, expires_at);
CREATE INDEX IF NOT EXISTS idx_shares_user_id ON shares(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
`
_, err := d.Exec(schema)
return err
}
func (d *Database) CreateShare(s *models.Share) error {
query := `
INSERT INTO shares (
id, user_id, user_display, is_guest, type, title, stored_path,
file_size, mime_type, text_content, views_count, downloads_count,
created_at, expires_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
_, err := d.Exec(query,
s.ID, s.UserID, s.UserDisplay, s.IsGuest, s.Type, s.Title, s.StoredPath,
s.FileSize, s.MimeType, s.TextContent, s.ViewsCount, s.DownloadsCount,
s.CreatedAt, s.ExpiresAt,
)
return err
}
func (d *Database) GetShare(id string) (*models.Share, error) {
query := `
SELECT id, user_id, user_display, is_guest, type, title, stored_path,
file_size, mime_type, text_content, views_count, downloads_count,
created_at, expires_at
FROM shares WHERE id = ?`
row := d.QueryRow(query, id)
s := &models.Share{}
var expiresAt sql.NullTime
var textContent, storedPath sql.NullString
var userID sql.NullString
err := row.Scan(
&s.ID, &userID, &s.UserDisplay, &s.IsGuest, &s.Type, &s.Title, &storedPath,
&s.FileSize, &s.MimeType, &textContent, &s.ViewsCount, &s.DownloadsCount,
&s.CreatedAt, &expiresAt,
)
if err != nil {
return nil, err
}
if userID.Valid {
s.UserID = &userID.String
}
if storedPath.Valid {
s.StoredPath = storedPath.String
}
if textContent.Valid {
s.TextContent = textContent.String
}
if expiresAt.Valid {
s.ExpiresAt = &expiresAt.Time
}
return s, nil
}
func (d *Database) IncrementViews(id string) error {
_, err := d.Exec("UPDATE shares SET views_count = views_count + 1 WHERE id = ?", id)
return err
}
func (d *Database) IncrementDownloads(id string) error {
_, err := d.Exec("UPDATE shares SET downloads_count = downloads_count + 1 WHERE id = ?", id)
return err
}
func (d *Database) GetUserShares(userID string) ([]*models.Share, error) {
query := `
SELECT id, user_id, user_display, is_guest, type, title, stored_path,
file_size, mime_type, text_content, views_count, downloads_count,
created_at, expires_at
FROM shares WHERE user_id = ? ORDER BY created_at DESC`
rows, err := d.Query(query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var list []*models.Share
for rows.Next() {
s := &models.Share{}
var expiresAt sql.NullTime
var textContent, storedPath sql.NullString
var uID sql.NullString
if err := rows.Scan(
&s.ID, &uID, &s.UserDisplay, &s.IsGuest, &s.Type, &s.Title, &storedPath,
&s.FileSize, &s.MimeType, &textContent, &s.ViewsCount, &s.DownloadsCount,
&s.CreatedAt, &expiresAt,
); err != nil {
return nil, err
}
if uID.Valid {
s.UserID = &uID.String
}
if storedPath.Valid {
s.StoredPath = storedPath.String
}
if textContent.Valid {
s.TextContent = textContent.String
}
if expiresAt.Valid {
s.ExpiresAt = &expiresAt.Time
}
list = append(list, s)
}
return list, nil
}
func (d *Database) DeleteShare(id string, userID *string) error {
if userID != nil {
_, err := d.Exec("DELETE FROM shares WHERE id = ? AND user_id = ?", id, *userID)
return err
}
_, err := d.Exec("DELETE FROM shares WHERE id = ?", id)
return err
}
// GetExpiredGuestShares obtiene SOLO los shares marcados como guest cuya fecha de expiración ya pasó.
// Los usuarios registrados (is_guest = 0) NUNCA se seleccionan aquí.
func (d *Database) GetExpiredGuestShares() ([]*models.Share, error) {
query := `
SELECT id, stored_path
FROM shares
WHERE is_guest = 1 AND expires_at IS NOT NULL AND expires_at <= CURRENT_TIMESTAMP`
rows, err := d.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var list []*models.Share
for rows.Next() {
s := &models.Share{}
var storedPath sql.NullString
if err := rows.Scan(&s.ID, &storedPath); err != nil {
return nil, err
}
if storedPath.Valid {
s.StoredPath = storedPath.String
}
list = append(list, s)
}
return list, nil
}
func (d *Database) PurgeShare(id string) error {
_, err := d.Exec("DELETE FROM shares WHERE id = ? AND is_guest = 1", id)
return err
}
func (d *Database) UpsertUser(u *models.User) error {
query := `
INSERT INTO users (id, provider, provider_id, email, name, avatar_url, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
email = excluded.email,
name = excluded.name,
avatar_url = excluded.avatar_url`
_, err := d.Exec(query, u.ID, u.Provider, u.ProviderID, u.Email, u.Name, u.AvatarURL, u.CreatedAt)
return err
}
func (d *Database) GetUser(id string) (*models.User, error) {
query := `SELECT id, provider, provider_id, email, name, avatar_url, created_at FROM users WHERE id = ?`
row := d.QueryRow(query, id)
u := &models.User{}
var email, name, avatar sql.NullString
if err := row.Scan(&u.ID, &u.Provider, &u.ProviderID, &email, &name, &avatar, &u.CreatedAt); err != nil {
return nil, err
}
if email.Valid {
u.Email = email.String
}
if name.Valid {
u.Name = name.String
}
if avatar.Valid {
u.AvatarURL = avatar.String
}
return u, nil
}
func (d *Database) CreateSession(s *models.Session) error {
query := `
INSERT INTO sessions (token, user_id, is_guest, guest_id, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?)`
_, err := d.Exec(query, s.Token, s.UserID, s.IsGuest, s.GuestID, s.CreatedAt, s.ExpiresAt)
return err
}
func (d *Database) GetSession(token string) (*models.Session, error) {
query := `
SELECT s.token, s.user_id, s.is_guest, s.guest_id, s.created_at, s.expires_at,
u.id, u.provider, u.provider_id, u.email, u.name, u.avatar_url, u.created_at
FROM sessions s
LEFT JOIN users u ON s.user_id = u.id
WHERE s.token = ? AND s.expires_at > CURRENT_TIMESTAMP`
row := d.QueryRow(query, token)
s := &models.Session{}
var userID sql.NullString
var uID, provider, provID, email, name, avatar sql.NullString
var uCreated sql.NullTime
err := row.Scan(
&s.Token, &userID, &s.IsGuest, &s.GuestID, &s.CreatedAt, &s.ExpiresAt,
&uID, &provider, &provID, &email, &name, &avatar, &uCreated,
)
if err != nil {
return nil, err
}
if userID.Valid {
s.UserID = &userID.String
}
if uID.Valid {
s.User = &models.User{
ID: uID.String,
Provider: provider.String,
ProviderID: provID.String,
Email: email.String,
Name: name.String,
AvatarURL: avatar.String,
CreatedAt: uCreated.Time,
}
}
return s, nil
}
func (d *Database) DeleteSession(token string) error {
_, err := d.Exec("DELETE FROM sessions WHERE token = ?", token)
return err
}
func (d *Database) PurgeExpiredSessions() error {
_, err := d.Exec("DELETE FROM sessions WHERE expires_at <= CURRENT_TIMESTAMP")
return err
}