All files / web/src/app/api/worksheets/share route.ts

0% Statements 0/114
0% Branches 0/1
0% Functions 0/1
0% Lines 0/114

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115                                                                                                                                                                                                                                     
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { db } from '@/db'
import { worksheetShares } from '@/db/schema'
import { generateShareId } from '@/lib/generateShareId'
import { serializeAdditionConfig } from '@/app/create/worksheets/config-schemas'
import { withAuth } from '@/lib/auth/withAuth'

/**
 * POST /api/worksheets/share
 *
 * Create a shareable link for a worksheet configuration
 *
 * Request body:
 * {
 *   worksheetType: 'addition' | 'subtraction' | ...,
 *   config: { ...worksheet config object },
 *   title?: string (optional description)
 * }
 *
 * Response:
 * {
 *   id: 'abc123X',
 *   url: 'https://abaci.one/worksheets/shared/abc123X'
 * }
 */
export const POST = withAuth(async (request) => {
  try {
    const body = await request.json()
    const { worksheetType, config, title } = body

    // Validate required fields
    if (!worksheetType || !config) {
      return NextResponse.json(
        { error: 'Missing required fields: worksheetType, config' },
        { status: 400 }
      )
    }

    // Validate worksheetType - only 'addition' is supported for now
    if (worksheetType !== 'addition') {
      return NextResponse.json(
        { error: `Unsupported worksheet type: ${worksheetType}` },
        { status: 400 }
      )
    }

    // Generate unique ID (retry if collision - extremely unlikely with base62^7)
    let shareId = generateShareId()
    let attempts = 0
    const MAX_ATTEMPTS = 5
    let isUnique = false

    while (!isUnique && attempts < MAX_ATTEMPTS) {
      shareId = generateShareId()
      const existing = await db.query.worksheetShares.findFirst({
        where: eq(worksheetShares.id, shareId),
      })

      if (!existing) {
        isUnique = true
      } else {
        attempts++
      }
    }

    if (!isUnique) {
      return NextResponse.json({ error: 'Failed to generate unique share ID' }, { status: 500 })
    }

    // Get creator IP (hashed for privacy)
    const forwardedFor = request.headers.get('x-forwarded-for')
    const ip = forwardedFor?.split(',')[0] || request.headers.get('x-real-ip') || 'unknown'

    // Simple hash function for IP (not cryptographic, just for basic spam prevention)
    const hashIp = (str: string) => {
      let hash = 0
      for (let i = 0; i < str.length; i++) {
        const char = str.charCodeAt(i)
        hash = (hash << 5) - hash + char
        hash = hash & hash // Convert to 32-bit integer
      }
      return hash.toString(36)
    }

    // Serialize config (adds version, validates structure)
    // This ensures the shared config uses the same format as database auto-save
    const configJson = serializeAdditionConfig(config)

    // Create share record
    await db.insert(worksheetShares).values({
      id: shareId,
      worksheetType,
      config: configJson,
      createdAt: new Date(),
      views: 0,
      creatorIp: hashIp(ip),
      title: title || null,
    })

    // Build full URL
    const protocol = request.headers.get('x-forwarded-proto') || 'https'
    const host = request.headers.get('host') || 'abaci.one'
    const url = `${protocol}://${host}/worksheets/shared/${shareId}`

    return NextResponse.json({
      id: shareId,
      url,
    })
  } catch (error) {
    console.error('Error creating worksheet share:', error)
    return NextResponse.json({ error: 'Failed to create share' }, { status: 500 })
  }
})