feat: implementar webapp app_compartir con visor universal, auth y purga 24h para guest
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user