Menu
Back to Blog
2 min read
Technologie

Digital Product Delivery

Digitale Produkte sicher ausliefern. Download-Links, License Keys, E-Books und Software mit Node.js und Next.js automatisiert bereitstellen.

Digital ProductsDownload LinksLicense KeysE-Book DeliverySoftware DistributionSigned URLs
Digital Product Delivery

Digital Product Delivery

Meta-Description: Digitale Produkte sicher ausliefern. Download-Links, License Keys, E-Books und Software mit Node.js und Next.js automatisiert bereitstellen.

Keywords: Digital Products, Download Links, License Keys, E-Book Delivery, Software Distribution, Signed URLs, Content Protection


Einführung

Digital Product Delivery automatisiert die Auslieferung von Downloads, Lizenzen und digitalen Gütern nach dem Kauf. Von signierten URLs bis License Key Generation – sichere Delivery ist entscheidend für digitale Geschäftsmodelle. Dieser Guide zeigt Production-ready Implementierungen.


Delivery Architecture

┌─────────────────────────────────────────────────────────────┐
│           DIGITAL PRODUCT DELIVERY SYSTEM                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                 PRODUCT TYPES                        │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │                                                     │   │
│  │  📁 Downloadable Files    📋 License Keys          │   │
│  │  ├── E-Books (PDF/EPUB)   ├── Software Licenses    │   │
│  │  ├── Audio/Video          ├── API Access Keys      │   │
│  │  ├── Software Packages    └── Subscription Codes   │   │
│  │  └── Templates/Assets                              │   │
│  │                                                     │   │
│  │  🔗 Access Grants          📧 Email Delivery       │   │
│  │  ├── Course Access         ├── Welcome Emails      │   │
│  │  ├── Membership Areas      ├── Download Links      │   │
│  │  └── Private Content       └── License Delivery    │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                          │                                  │
│                          ▼                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              DELIVERY PIPELINE                       │   │
│  │                                                     │   │
│  │  Payment ──► Fulfillment ──► Delivery ──► Access   │   │
│  │     │            │              │            │      │   │
│  │  Webhook    Generate      Send Email    Track      │   │
│  │  Received   Assets/Keys   with Links    Downloads  │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Security Measures:                                        │
│  ├── Signed URLs with expiration                          │
│  ├── Download limits per purchase                         │
│  ├── IP tracking and fraud detection                      │
│  └── License key validation                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Product & License Models

// prisma/schema.prisma
model DigitalProduct {
  id            String   @id @default(cuid())
  name          String
  slug          String   @unique
  description   String?
  type          ProductType
  price         Int      // cents
  currency      String   @default("EUR")

  // File-based products
  fileKey       String?  // S3 key
  fileName      String?
  fileSize      Int?
  fileMimeType  String?

  // License-based products
  licenseType   LicenseType?
  maxActivations Int?

  // Metadata
  metadata      Json?
  active        Boolean  @default(true)
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  purchases     Purchase[]
  licenses      License[]
}

model Purchase {
  id            String   @id @default(cuid())
  userId        String
  productId     String
  product       DigitalProduct @relation(fields: [productId], references: [id])

  // Payment info
  stripePaymentId String?
  amount        Int
  currency      String
  status        PurchaseStatus @default(PENDING)

  // Delivery tracking
  deliveredAt   DateTime?
  downloadCount Int      @default(0)
  maxDownloads  Int      @default(5)

  // Download links
  downloads     Download[]
  licenses      License[]

  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  user          User     @relation(fields: [userId], references: [id])
}

model Download {
  id          String   @id @default(cuid())
  purchaseId  String
  purchase    Purchase @relation(fields: [purchaseId], references: [id])

  token       String   @unique
  expiresAt   DateTime
  downloadedAt DateTime?
  ipAddress   String?
  userAgent   String?

  createdAt   DateTime @default(now())
}

model License {
  id            String   @id @default(cuid())
  purchaseId    String
  purchase      Purchase @relation(fields: [purchaseId], references: [id])
  productId     String
  product       DigitalProduct @relation(fields: [productId], references: [id])

  key           String   @unique
  status        LicenseStatus @default(ACTIVE)
  activations   Int      @default(0)
  maxActivations Int

  // Activation tracking
  activationRecords LicenseActivation[]

  expiresAt     DateTime?
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

model LicenseActivation {
  id          String   @id @default(cuid())
  licenseId   String
  license     License  @relation(fields: [licenseId], references: [id])

  machineId   String
  machineName String?
  ipAddress   String?
  activatedAt DateTime @default(now())
  deactivatedAt DateTime?

  @@unique([licenseId, machineId])
}

enum ProductType {
  DOWNLOAD      // Downloadable file
  LICENSE       // Software license
  SUBSCRIPTION  // Recurring access
  COURSE        // Online course access
  MEMBERSHIP    // Membership access
}

enum LicenseType {
  PERPETUAL     // Forever
  SUBSCRIPTION  // Time-limited
  TRIAL         // Trial period
}

enum PurchaseStatus {
  PENDING
  COMPLETED
  REFUNDED
  FAILED
}

enum LicenseStatus {
  ACTIVE
  SUSPENDED
  REVOKED
  EXPIRED
}

Signed URL Generation

// lib/delivery/signed-urls.ts
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import crypto from 'crypto';

const s3 = new S3Client({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
  }
});

interface SignedUrlOptions {
  expiresIn?: number; // seconds
  downloadFilename?: string;
}

export async function generateSignedDownloadUrl(
  fileKey: string,
  options: SignedUrlOptions = {}
): Promise<string> {
  const { expiresIn = 3600, downloadFilename } = options;

  const command = new GetObjectCommand({
    Bucket: process.env.S3_BUCKET!,
    Key: fileKey,
    ResponseContentDisposition: downloadFilename
      ? `attachment; filename="${downloadFilename}"`
      : undefined
  });

  return getSignedUrl(s3, command, { expiresIn });
}

// Custom signed URL for self-hosted files
export function generateCustomSignedUrl(
  path: string,
  expiresIn: number = 3600
): string {
  const expires = Math.floor(Date.now() / 1000) + expiresIn;
  const signature = crypto
    .createHmac('sha256', process.env.DOWNLOAD_SECRET!)
    .update(`${path}:${expires}`)
    .digest('hex');

  const baseUrl = process.env.NEXT_PUBLIC_APP_URL;
  return `${baseUrl}/api/download/${path}?expires=${expires}&sig=${signature}`;
}

export function verifySignedUrl(
  path: string,
  expires: string,
  signature: string
): boolean {
  const expiresNum = parseInt(expires, 10);

  // Check expiration
  if (Date.now() / 1000 > expiresNum) {
    return false;
  }

  // Verify signature
  const expectedSig = crypto
    .createHmac('sha256', process.env.DOWNLOAD_SECRET!)
    .update(`${path}:${expiresNum}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSig)
  );
}

Download Token System

// lib/delivery/download-tokens.ts
import { db } from '@/lib/db';
import crypto from 'crypto';
import { addHours } from 'date-fns';

interface CreateDownloadTokenResult {
  token: string;
  downloadUrl: string;
  expiresAt: Date;
}

export async function createDownloadToken(
  purchaseId: string,
  expirationHours: number = 24
): Promise<CreateDownloadTokenResult> {
  const purchase = await db.purchase.findUnique({
    where: { id: purchaseId },
    include: { product: true }
  });

  if (!purchase) {
    throw new Error('Purchase not found');
  }

  if (purchase.downloadCount >= purchase.maxDownloads) {
    throw new Error('Download limit exceeded');
  }

  const token = crypto.randomBytes(32).toString('hex');
  const expiresAt = addHours(new Date(), expirationHours);

  await db.download.create({
    data: {
      purchaseId,
      token,
      expiresAt
    }
  });

  const downloadUrl = `${process.env.NEXT_PUBLIC_APP_URL}/api/download/${token}`;

  return { token, downloadUrl, expiresAt };
}

export async function validateAndConsumeToken(
  token: string,
  ipAddress?: string,
  userAgent?: string
): Promise<{ fileKey: string; fileName: string } | null> {
  const download = await db.download.findUnique({
    where: { token },
    include: {
      purchase: {
        include: { product: true }
      }
    }
  });

  if (!download) {
    return null;
  }

  // Check expiration
  if (download.expiresAt < new Date()) {
    return null;
  }

  // Check if already used
  if (download.downloadedAt) {
    return null;
  }

  // Check purchase download limits
  if (download.purchase.downloadCount >= download.purchase.maxDownloads) {
    return null;
  }

  // Mark as used and increment counter
  await db.$transaction([
    db.download.update({
      where: { id: download.id },
      data: {
        downloadedAt: new Date(),
        ipAddress,
        userAgent
      }
    }),
    db.purchase.update({
      where: { id: download.purchaseId },
      data: {
        downloadCount: { increment: 1 }
      }
    })
  ]);

  return {
    fileKey: download.purchase.product.fileKey!,
    fileName: download.purchase.product.fileName!
  };
}

License Key Generation

// lib/delivery/license-keys.ts
import crypto from 'crypto';
import { db } from '@/lib/db';

interface LicenseKeyOptions {
  prefix?: string;
  segments?: number;
  segmentLength?: number;
}

export function generateLicenseKey(options: LicenseKeyOptions = {}): string {
  const {
    prefix = 'PRO',
    segments = 4,
    segmentLength = 5
  } = options;

  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // No I, O, 0, 1
  const keySegments: string[] = [];

  for (let i = 0; i < segments; i++) {
    let segment = '';
    for (let j = 0; j < segmentLength; j++) {
      const randomIndex = crypto.randomInt(0, chars.length);
      segment += chars[randomIndex];
    }
    keySegments.push(segment);
  }

  return `${prefix}-${keySegments.join('-')}`;
}

// Checksum-validated license key
export function generateValidatedLicenseKey(productCode: string): string {
  const key = generateLicenseKey({ prefix: productCode });
  const checksum = calculateChecksum(key);
  return `${key}-${checksum}`;
}

function calculateChecksum(key: string): string {
  const hash = crypto
    .createHash('sha256')
    .update(key + process.env.LICENSE_SECRET!)
    .digest('hex');
  return hash.substring(0, 4).toUpperCase();
}

export function validateLicenseKeyFormat(key: string): boolean {
  // Format: XXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXX
  const pattern = /^[A-Z]{2,4}-([A-Z0-9]{5}-){3,4}[A-Z0-9]{4}$/;

  if (!pattern.test(key)) {
    return false;
  }

  // Validate checksum
  const parts = key.split('-');
  const checksum = parts.pop()!;
  const keyWithoutChecksum = parts.join('-');

  return calculateChecksum(keyWithoutChecksum) === checksum;
}

// Create license in database
export async function createLicense(
  purchaseId: string,
  productId: string
): Promise<License> {
  const product = await db.digitalProduct.findUnique({
    where: { id: productId }
  });

  if (!product) {
    throw new Error('Product not found');
  }

  const key = generateValidatedLicenseKey(
    product.slug.substring(0, 3).toUpperCase()
  );

  return db.license.create({
    data: {
      purchaseId,
      productId,
      key,
      maxActivations: product.maxActivations || 3,
      expiresAt: product.licenseType === 'SUBSCRIPTION'
        ? new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year
        : null
    }
  });
}

License Activation API

// app/api/licenses/activate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { validateLicenseKeyFormat } from '@/lib/delivery/license-keys';

interface ActivationRequest {
  licenseKey: string;
  machineId: string;
  machineName?: string;
}

export async function POST(request: NextRequest) {
  try {
    const body: ActivationRequest = await request.json();
    const { licenseKey, machineId, machineName } = body;

    // Validate format
    if (!validateLicenseKeyFormat(licenseKey)) {
      return NextResponse.json(
        { error: 'Invalid license key format' },
        { status: 400 }
      );
    }

    // Find license
    const license = await db.license.findUnique({
      where: { key: licenseKey },
      include: {
        product: true,
        activationRecords: true
      }
    });

    if (!license) {
      return NextResponse.json(
        { error: 'License not found' },
        { status: 404 }
      );
    }

    // Check status
    if (license.status !== 'ACTIVE') {
      return NextResponse.json(
        { error: `License is ${license.status.toLowerCase()}` },
        { status: 403 }
      );
    }

    // Check expiration
    if (license.expiresAt && license.expiresAt < new Date()) {
      await db.license.update({
        where: { id: license.id },
        data: { status: 'EXPIRED' }
      });

      return NextResponse.json(
        { error: 'License has expired' },
        { status: 403 }
      );
    }

    // Check if already activated on this machine
    const existingActivation = license.activationRecords.find(
      a => a.machineId === machineId && !a.deactivatedAt
    );

    if (existingActivation) {
      return NextResponse.json({
        success: true,
        message: 'Already activated on this machine',
        license: formatLicenseResponse(license)
      });
    }

    // Check activation limit
    const activeActivations = license.activationRecords.filter(
      a => !a.deactivatedAt
    ).length;

    if (activeActivations >= license.maxActivations) {
      return NextResponse.json(
        {
          error: 'Maximum activations reached',
          maxActivations: license.maxActivations,
          currentActivations: activeActivations
        },
        { status: 403 }
      );
    }

    // Create activation
    const ipAddress = request.headers.get('x-forwarded-for') ||
                      request.headers.get('x-real-ip');

    await db.$transaction([
      db.licenseActivation.create({
        data: {
          licenseId: license.id,
          machineId,
          machineName,
          ipAddress
        }
      }),
      db.license.update({
        where: { id: license.id },
        data: { activations: { increment: 1 } }
      })
    ]);

    return NextResponse.json({
      success: true,
      message: 'License activated successfully',
      license: formatLicenseResponse(license)
    });

  } catch (error) {
    console.error('Activation error:', error);
    return NextResponse.json(
      { error: 'Activation failed' },
      { status: 500 }
    );
  }
}

function formatLicenseResponse(license: any) {
  return {
    productName: license.product.name,
    licenseType: license.product.licenseType,
    expiresAt: license.expiresAt,
    features: license.product.metadata?.features || []
  };
}

// Deactivation endpoint
export async function DELETE(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const licenseKey = searchParams.get('key');
  const machineId = searchParams.get('machineId');

  if (!licenseKey || !machineId) {
    return NextResponse.json(
      { error: 'License key and machine ID required' },
      { status: 400 }
    );
  }

  const license = await db.license.findUnique({
    where: { key: licenseKey }
  });

  if (!license) {
    return NextResponse.json(
      { error: 'License not found' },
      { status: 404 }
    );
  }

  const activation = await db.licenseActivation.findUnique({
    where: {
      licenseId_machineId: {
        licenseId: license.id,
        machineId
      }
    }
  });

  if (!activation || activation.deactivatedAt) {
    return NextResponse.json(
      { error: 'No active activation found' },
      { status: 404 }
    );
  }

  await db.$transaction([
    db.licenseActivation.update({
      where: { id: activation.id },
      data: { deactivatedAt: new Date() }
    }),
    db.license.update({
      where: { id: license.id },
      data: { activations: { decrement: 1 } }
    })
  ]);

  return NextResponse.json({
    success: true,
    message: 'License deactivated successfully'
  });
}

Fulfillment Service

// lib/delivery/fulfillment.ts
import { db } from '@/lib/db';
import { createDownloadToken } from './download-tokens';
import { createLicense } from './license-keys';
import { sendEmail } from '@/lib/email';

interface FulfillmentResult {
  success: boolean;
  downloadUrl?: string;
  licenseKey?: string;
  error?: string;
}

export async function fulfillPurchase(
  purchaseId: string
): Promise<FulfillmentResult> {
  const purchase = await db.purchase.findUnique({
    where: { id: purchaseId },
    include: {
      product: true,
      user: true
    }
  });

  if (!purchase) {
    return { success: false, error: 'Purchase not found' };
  }

  if (purchase.status !== 'COMPLETED') {
    return { success: false, error: 'Purchase not completed' };
  }

  if (purchase.deliveredAt) {
    return { success: false, error: 'Already delivered' };
  }

  try {
    let downloadUrl: string | undefined;
    let licenseKey: string | undefined;

    const { product } = purchase;

    // Handle different product types
    switch (product.type) {
      case 'DOWNLOAD':
        const downloadResult = await createDownloadToken(purchaseId);
        downloadUrl = downloadResult.downloadUrl;
        break;

      case 'LICENSE':
        const license = await createLicense(purchaseId, product.id);
        licenseKey = license.key;
        break;

      case 'SUBSCRIPTION':
      case 'MEMBERSHIP':
        // Grant access in database
        await grantAccess(purchase.userId, product.id);
        break;

      case 'COURSE':
        await enrollInCourse(purchase.userId, product.id);
        break;
    }

    // Mark as delivered
    await db.purchase.update({
      where: { id: purchaseId },
      data: { deliveredAt: new Date() }
    });

    // Send delivery email
    await sendDeliveryEmail(purchase, downloadUrl, licenseKey);

    return { success: true, downloadUrl, licenseKey };

  } catch (error) {
    console.error('Fulfillment error:', error);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Fulfillment failed'
    };
  }
}

async function grantAccess(userId: string, productId: string): Promise<void> {
  await db.userAccess.create({
    data: {
      userId,
      productId,
      grantedAt: new Date()
    }
  });
}

async function enrollInCourse(userId: string, productId: string): Promise<void> {
  const product = await db.digitalProduct.findUnique({
    where: { id: productId }
  });

  const courseId = product?.metadata?.courseId;
  if (!courseId) {
    throw new Error('Course ID not found in product metadata');
  }

  await db.courseEnrollment.create({
    data: {
      userId,
      courseId,
      enrolledAt: new Date()
    }
  });
}

async function sendDeliveryEmail(
  purchase: any,
  downloadUrl?: string,
  licenseKey?: string
): Promise<void> {
  const { user, product } = purchase;

  await sendEmail({
    to: user.email,
    template: 'product-delivery',
    data: {
      userName: user.name,
      productName: product.name,
      productType: product.type,
      downloadUrl,
      licenseKey,
      supportEmail: 'support@example.com'
    }
  });
}

Download API Endpoint

// app/api/download/[token]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { validateAndConsumeToken } from '@/lib/delivery/download-tokens';

const s3 = new S3Client({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
  }
});

export async function GET(
  request: NextRequest,
  { params }: { params: { token: string } }
) {
  const { token } = params;

  const ipAddress = request.headers.get('x-forwarded-for') ||
                    request.headers.get('x-real-ip') ||
                    undefined;
  const userAgent = request.headers.get('user-agent') || undefined;

  // Validate and consume token
  const result = await validateAndConsumeToken(token, ipAddress, userAgent);

  if (!result) {
    return NextResponse.json(
      { error: 'Invalid or expired download link' },
      { status: 403 }
    );
  }

  try {
    // Fetch file from S3
    const command = new GetObjectCommand({
      Bucket: process.env.S3_BUCKET!,
      Key: result.fileKey
    });

    const s3Response = await s3.send(command);

    if (!s3Response.Body) {
      throw new Error('File not found');
    }

    // Stream the file
    const stream = s3Response.Body as ReadableStream;

    return new NextResponse(stream, {
      headers: {
        'Content-Type': s3Response.ContentType || 'application/octet-stream',
        'Content-Disposition': `attachment; filename="${result.fileName}"`,
        'Content-Length': s3Response.ContentLength?.toString() || '',
        'Cache-Control': 'no-store'
      }
    });

  } catch (error) {
    console.error('Download error:', error);
    return NextResponse.json(
      { error: 'Download failed' },
      { status: 500 }
    );
  }
}

Stripe Webhook Integration

// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { db } from '@/lib/db';
import { fulfillPurchase } from '@/lib/delivery/fulfillment';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (error) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 400 }
    );
  }

  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object as Stripe.Checkout.Session);
      break;

    case 'payment_intent.succeeded':
      await handlePaymentSucceeded(event.data.object as Stripe.PaymentIntent);
      break;

    case 'charge.refunded':
      await handleRefund(event.data.object as Stripe.Charge);
      break;
  }

  return NextResponse.json({ received: true });
}

async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
  const purchaseId = session.metadata?.purchaseId;

  if (!purchaseId) {
    console.error('No purchase ID in session metadata');
    return;
  }

  // Update purchase status
  await db.purchase.update({
    where: { id: purchaseId },
    data: {
      status: 'COMPLETED',
      stripePaymentId: session.payment_intent as string
    }
  });

  // Fulfill the purchase
  const result = await fulfillPurchase(purchaseId);

  if (!result.success) {
    console.error('Fulfillment failed:', result.error);
    // Alert for manual intervention
    await alertFulfillmentFailure(purchaseId, result.error);
  }
}

async function handlePaymentSucceeded(paymentIntent: Stripe.PaymentIntent) {
  // Handle one-time payments
  const purchase = await db.purchase.findFirst({
    where: { stripePaymentId: paymentIntent.id }
  });

  if (purchase && purchase.status !== 'COMPLETED') {
    await db.purchase.update({
      where: { id: purchase.id },
      data: { status: 'COMPLETED' }
    });

    await fulfillPurchase(purchase.id);
  }
}

async function handleRefund(charge: Stripe.Charge) {
  const purchase = await db.purchase.findFirst({
    where: { stripePaymentId: charge.payment_intent as string }
  });

  if (purchase) {
    await db.purchase.update({
      where: { id: purchase.id },
      data: { status: 'REFUNDED' }
    });

    // Revoke licenses
    await db.license.updateMany({
      where: { purchaseId: purchase.id },
      data: { status: 'REVOKED' }
    });

    // Revoke access
    await db.userAccess.deleteMany({
      where: {
        userId: purchase.userId,
        productId: purchase.productId
      }
    });
  }
}

async function alertFulfillmentFailure(purchaseId: string, error?: string) {
  // Send alert to admin
  await sendEmail({
    to: process.env.ADMIN_EMAIL!,
    template: 'fulfillment-failure',
    data: { purchaseId, error }
  });
}

Customer Download Page

// app/purchases/[id]/page.tsx
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { createDownloadToken } from '@/lib/delivery/download-tokens';
import { redirect, notFound } from 'next/navigation';

interface Props {
  params: { id: string };
}

export default async function PurchasePage({ params }: Props) {
  const session = await auth();
  if (!session?.user) redirect('/login');

  const purchase = await db.purchase.findUnique({
    where: {
      id: params.id,
      userId: session.user.id
    },
    include: {
      product: true,
      licenses: true,
      downloads: {
        orderBy: { createdAt: 'desc' },
        take: 5
      }
    }
  });

  if (!purchase) notFound();

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">{purchase.product.name}</h1>

      <div className="bg-white rounded-lg shadow p-6 space-y-6">
        {/* Purchase Info */}
        <div className="border-b pb-4">
          <p className="text-sm text-gray-500">Purchase Date</p>
          <p>{new Date(purchase.createdAt).toLocaleDateString()}</p>
        </div>

        {/* Download Section */}
        {purchase.product.type === 'DOWNLOAD' && (
          <DownloadSection purchase={purchase} />
        )}

        {/* License Section */}
        {purchase.product.type === 'LICENSE' && (
          <LicenseSection licenses={purchase.licenses} />
        )}

        {/* Access Section */}
        {['SUBSCRIPTION', 'MEMBERSHIP', 'COURSE'].includes(purchase.product.type) && (
          <AccessSection product={purchase.product} />
        )}
      </div>
    </div>
  );
}

function DownloadSection({ purchase }: { purchase: any }) {
  const remainingDownloads = purchase.maxDownloads - purchase.downloadCount;

  return (
    <div>
      <h2 className="font-semibold mb-2">Download</h2>
      <p className="text-sm text-gray-600 mb-4">
        {remainingDownloads} downloads remaining
      </p>

      {remainingDownloads > 0 ? (
        <form action={generateNewDownload}>
          <input type="hidden" name="purchaseId" value={purchase.id} />
          <button
            type="submit"
            className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
          >
            Generate Download Link
          </button>
        </form>
      ) : (
        <p className="text-red-600">Download limit reached. Contact support.</p>
      )}

      {/* Recent Downloads */}
      {purchase.downloads.length > 0 && (
        <div className="mt-4">
          <h3 className="text-sm font-medium mb-2">Recent Downloads</h3>
          <ul className="text-sm text-gray-600 space-y-1">
            {purchase.downloads.map((d: any) => (
              <li key={d.id}>
                {new Date(d.createdAt).toLocaleString()} -
                {d.downloadedAt ? ' Downloaded' : ' Pending'}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

function LicenseSection({ licenses }: { licenses: any[] }) {
  return (
    <div>
      <h2 className="font-semibold mb-2">License Keys</h2>
      {licenses.map(license => (
        <div key={license.id} className="bg-gray-50 p-4 rounded mb-2">
          <code className="text-lg font-mono">{license.key}</code>
          <div className="text-sm text-gray-600 mt-2">
            <p>Status: {license.status}</p>
            <p>Activations: {license.activations} / {license.maxActivations}</p>
            {license.expiresAt && (
              <p>Expires: {new Date(license.expiresAt).toLocaleDateString()}</p>
            )}
          </div>
        </div>
      ))}
    </div>
  );
}

function AccessSection({ product }: { product: any }) {
  const accessUrl = getAccessUrl(product);

  return (
    <div>
      <h2 className="font-semibold mb-2">Access</h2>
      <a
        href={accessUrl}
        className="inline-block bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
      >
        Access {product.name}
      </a>
    </div>
  );
}

async function generateNewDownload(formData: FormData) {
  'use server';

  const purchaseId = formData.get('purchaseId') as string;
  const result = await createDownloadToken(purchaseId);

  redirect(result.downloadUrl);
}

function getAccessUrl(product: any): string {
  switch (product.type) {
    case 'COURSE':
      return `/courses/${product.metadata?.courseSlug}`;
    case 'MEMBERSHIP':
      return `/members`;
    default:
      return `/dashboard`;
  }
}

Best Practices

AspectRecommendation
**URLs**Signed, time-limited, single-use
**Limits**3-5 downloads per purchase
**Licenses**Checksum validation, activation limits
**Email**Immediate delivery confirmation
**Refunds**Auto-revoke access and licenses
**Monitoring**Track failed deliveries, alert admins

Fazit

Digital Product Delivery erfordert:

  1. Security: Signierte URLs, Token, validierte Keys
  2. Automation: Webhook-triggered Fulfillment
  3. Limits: Download-Counts, Activation-Limits
  4. UX: Klare Download-Pages, License-Management

Sichere Delivery schützt digitale Produkte und sorgt für gute Customer Experience.


Bildprompts

  1. "Digital product delivery flow diagram, download and license paths"
  2. "License key activation interface, software registration screen"
  3. "E-commerce download page, purchase confirmation with download button"

Quellen