- Proto definitions (engpad.v1.EngPadSync) with 6 RPCs - Generated Go gRPC code - Auth interceptor: username/password from metadata - SyncNotebook: upsert with full page/stroke replacement in a tx - DeleteNotebook, ListNotebooks handlers - Share link RPCs: CreateShareLink, RevokeShareLink, ListShareLinks - Share link token management (32-byte random, optional expiry) - gRPC server with TLS 1.3 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
106 lines
2.6 KiB
Go
106 lines
2.6 KiB
Go
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
|
|
}
|