package share import ( "crypto/rand" "database/sql" "encoding/base64" "fmt" "time" ) const tokenBytes = 32 type LinkInfo struct { Token string URL string CreatedAt time.Time ExpiresAt *time.Time } // CreateLink generates a shareable link for a notebook. func CreateLink(database *sql.DB, notebookID int64, expiry time.Duration, baseURL string) (string, *time.Time, error) { raw := make([]byte, tokenBytes) if _, err := rand.Read(raw); err != nil { return "", nil, fmt.Errorf("generate token: %w", err) } token := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(raw) now := time.Now().UnixMilli() var expiresAt *int64 var expiresTime *time.Time if expiry != 0 { ea := time.Now().Add(expiry).UnixMilli() expiresAt = &ea t := time.UnixMilli(ea) expiresTime = &t } _, err := database.Exec( "INSERT INTO share_links (notebook_id, token, expires_at, created_at) VALUES (?, ?, ?, ?)", notebookID, token, expiresAt, now, ) if err != nil { return "", nil, fmt.Errorf("insert share link: %w", err) } return token, expiresTime, nil } // ValidateLink checks if a token is valid and returns the notebook ID. func ValidateLink(database *sql.DB, token string) (int64, error) { var notebookID int64 var expiresAt *int64 err := database.QueryRow( "SELECT notebook_id, expires_at FROM share_links WHERE token = ?", token, ).Scan(¬ebookID, &expiresAt) if err != nil { return 0, fmt.Errorf("link not found") } if expiresAt != nil && time.Now().UnixMilli() > *expiresAt { return 0, fmt.Errorf("link expired") } return notebookID, nil } // RevokeLink deletes a share link. func RevokeLink(database *sql.DB, token string) error { _, err := database.Exec("DELETE FROM share_links WHERE token = ?", token) return err } // ListLinks returns all active share links for a notebook. func ListLinks(database *sql.DB, notebookID int64, baseURL string) ([]LinkInfo, error) { rows, err := database.Query( "SELECT token, created_at, expires_at FROM share_links WHERE notebook_id = ? ORDER BY created_at DESC", notebookID, ) if err != nil { return nil, err } defer func() { _ = rows.Close() }() var links []LinkInfo for rows.Next() { var token string var createdAt int64 var expiresAt *int64 if err := rows.Scan(&token, &createdAt, &expiresAt); err != nil { return nil, err } li := LinkInfo{ Token: token, URL: baseURL + "/s/" + token, CreatedAt: time.UnixMilli(createdAt), } if expiresAt != nil { t := time.UnixMilli(*expiresAt) li.ExpiresAt = &t } links = append(links, li) } return links, nil }