Skip to content

feat: add snippet sharing via unique public links#98

Open
Codex723 wants to merge 1 commit into
SudiptaPaul-31:mainfrom
Codex723:feature/snippet-sharing-links
Open

feat: add snippet sharing via unique public links#98
Codex723 wants to merge 1 commit into
SudiptaPaul-31:mainfrom
Codex723:feature/snippet-sharing-links

Conversation

@Codex723
Copy link
Copy Markdown

@Codex723 Codex723 commented Jun 2, 2026

Closes #72

Summary

This PR implements secure snippet sharing via unique public links with read-only permissions and expiration support.

Changes

Database Schema (scripts/add-snippet-shares.sql)

  • Created snippet_shares table with the following columns:
    • id - UUID primary key for each share record
    • snippet_id - Foreign key referencing snippets table (CASCADE delete)
    • share_token - 64-character hex string (cryptographically secure random)
    • is_read_only - Boolean flag (default: true) for access control
    • expires_at - Optional timestamp for automatic link expiration
    • created_by_wallet_address - Stellar wallet address of the snippet owner
    • revoked_at - Timestamp when share was revoked (nullable)
    • revoked_by_wallet_address - Wallet address that revoked the share (nullable)
    • created_at - Creation timestamp
  • Added indexes for performance:
    • idx_snippet_shares_token on share_token for O(1) lookups
    • idx_snippet_shares_snippet_id on snippet_id for ownership queries
    • idx_snippet_shares_expires_at for expiration cleanup
    • idx_snippet_shares_active composite index for active share queries

Repository Layer (app/api/snippets/share.repository.ts)

  • ShareRepository class with methods:
    • generateSecureToken() - Creates 32-byte random hex token (64 chars)
    • createShare(dto) - Inserts new share record with secure token
    • findByToken(token) - Finds active, non-expired share by token
    • findActiveShareBySnippet(snippetId) - Gets existing active share for snippet
    • revokeShare(snippetId, revokedByWalletAddress) - Revokes active share
    • revokeByToken(shareToken, revokedByWalletAddress) - Revokes by token directly
    • getShareDetails(snippetId) - Gets share history for a snippet

Service Layer (app/api/snippets/share.service.ts)

  • ShareService class with methods:
    • createShareLink(data) - Creates share link, returns existing if already exists
    • getSharedSnippet(shareToken) - Retrieves snippet via share token with read-only flag
    • revokeShare(snippetId, revokedByWalletAddress) - Revokes share with activity logging
  • Integrated with ActivityLogger for SHARE and REVOKESHARE audit trails

API Routes

app/api/snippets/[id]/share/route.ts

  • POST - Generate share link for a snippet

    • Validates wallet ownership via OwnershipMiddleware
    • Body: { isReadOnly?: boolean, expiresAt?: ISO string }
    • Returns: { shareToken, shareUrl, isReadOnly, expiresAt }
    • Rate limiting: 10 requests/minute per IP
    • Response codes: 201 (created), 401 (unauthorized), 403 (forbidden), 400 (validation), 500 (error)
  • DELETE - Revoke existing share link

    • Validates wallet ownership
    • Returns: { message: "Share link revoked successfully" }
    • Response codes: 200 (success), 401 (unauthorized), 403 (forbidden), 404 (no active share)

app/api/snippets/shared/[token]/route.ts

  • GET - Access shared snippet via public link (no auth required)
    • Path: /api/snippets/shared/{share_token}
    • Returns: { snippet, isReadOnly, shareUrl }
    • Response codes: 200 (success), 400 (missing token), 404 (invalid/expired), 500 (error)

Activity Logger (lib/activity-logger.ts)

  • Added SHARE and REVOKESHARE to ActivityAction type
  • Share actions are now logged with token and permissions metadata

Tests (lib/share.service.test.ts)

  • Unit tests for ShareService:
    • createShareLink - snippet not found, existing share, new share creation
    • getSharedSnippet - invalid token, valid token scenarios
    • revokeShare - no active share, successful revocation

Security Features

  1. Non-guessable tokens: 64-character hex (32 bytes) cryptographically random tokens
  2. Read-only permissions: Users accessing shared snippets get view-only access by default
  3. Expiration support: Optional expiresAt parameter for time-limited shares
  4. Revocation: Share links can be revoked by snippet owner
  5. Ownership verification: Only snippet owner can create/revoke share links
  6. Soft delete integration: Revoked shares preserve audit trail

API Response Format

Consistent JSON responses for frontend integration:

// POST /api/snippets/[id]/share - Success (201)
{
  "shareToken": "a1b2c3d4...",
  "shareUrl": "http://localhost:3000/api/snippets/shared/a1b2c3d4...",
  "isReadOnly": true,
  "expiresAt": null
}

// GET /api/snippets/shared/[token] - Success (200)
{
  "snippet": { "id": "...", "title": "...", ... },
  "isReadOnly": true,
  "shareUrl": "..."
}

Database Migration Required

Run scripts/add-snippet-shares.sql on your Neon database to create the snippet_shares table and indexes.

- Add snippet_shares table with UUID-based secure tokens
- Support read-only permissions for shared snippets
- Support link expiration via expires_at field
- Add SHARE and REVOKESHARE activity logging
- Create ShareRepository for database operations
- Create ShareService for business logic
- Create POST /api/snippets/[id]/share endpoint for generating share links
- Create DELETE /api/snippets/[id]/share endpoint for revoking share links
- Create GET /api/snippets/shared/[token] endpoint for accessing shared snippets
- Add unit tests for ShareService
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

@Codex723 is attempting to deploy a commit to the Sudipta 's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented Jun 2, 2026

@Codex723 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@SudiptaPaul-31
Copy link
Copy Markdown
Owner

@Codex723 resolve conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Snippet Sharing via Unique Public Links

2 participants