Files
eng-pad-server/internal/share/share.go
Kyle Isom 7d4e52ae92 Implement Phase 4: gRPC sync service
- 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>
2026-03-24 19:52:47 -07:00

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(&notebookID, &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
}