在线剪贴板源码分享

使用deno.com平台。已接入NL
演示:https://aut.deno.dev/

import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { crypto } from "https://deno.land/[email protected]/crypto/mod.ts";
import { encodeBase64, decodeBase64 } from "https://deno.land/[email protected]/encoding/base64.ts";

// 配置 - 只使用环境变量
const OAUTH2_BASE_URL = Deno.env.get("OAUTH2_BASE_URL");
const OAUTH2_CLIENT_ID = Deno.env.get("OAUTH2_CLIENT_ID");
const OAUTH2_CLIENT_SECRET = Deno.env.get("OAUTH2_CLIENT_SECRET");
const OAUTH2_REDIRECT_URI = Deno.env.get("OAUTH2_REDIRECT_URI");
const JWT_SECRET = Deno.env.get("JWT_SECRET") || "default-jwt-secret-change-me";

// 检查是否配置完整
function isConfigured(): boolean {
  return !!(OAUTH2_BASE_URL && OAUTH2_CLIENT_ID && OAUTH2_CLIENT_SECRET && OAUTH2_REDIRECT_URI);
}

// 全局变量
let kv: Deno.Kv;
const userSessions = new Map<string, any>();

// 初始化 KV
async function initKV() {
  kv = await Deno.openKv();
  console.log("KV initialized");
}

// 自动清理功能
async function cleanupEmptyClipboards() {
  try {
    console.log("🧹 开始清理空内容剪贴板...");
    
    const entries = kv.list({ prefix: ['clipboards'] });
    let cleanedCount = 0;
    let totalCount = 0;
    
    for await (const entry of entries) {
      totalCount++;
      const clipboard = entry.value as any;
      
      if (!clipboard) continue;
      
      // 检查是否为空内容
      let isEmpty = false;
      
      if (clipboard.encrypted) {
        // 对于加密内容,检查 has_content 字段
        isEmpty = !clipboard.has_content;
      } else {
        // 对于非加密内容,直接检查内容
        isEmpty = !clipboard.content || clipboard.content.trim().length === 0;
      }
      
      if (isEmpty) {
        await kv.delete(entry.key);
        cleanedCount++;
        console.log(`🗑️ 删除空剪贴板: ${entry.key[1]}`);
      }
    }
    
    console.log(`✅ 清理完成: 检查了 ${totalCount} 个剪贴板,删除了 ${cleanedCount} 个空剪贴板`);
  } catch (error) {
    console.error("❌ 清理过程中出错:", error);
  }
}

async function cleanupExpiredClipboards() {
  try {
    console.log("⏰ 开始清理过期剪贴板...");
    
    const entries = kv.list({ prefix: ['clipboards'] });
    let cleanedCount = 0;
    let totalCount = 0;
    const now = new Date();
    
    for await (const entry of entries) {
      totalCount++;
      const clipboard = entry.value as any;
      
      if (!clipboard) continue;
      
      // 检查是否过期
      if (clipboard.expires_at && new Date(clipboard.expires_at) < now) {
        await kv.delete(entry.key);
        cleanedCount++;
        console.log(`⏰ 删除过期剪贴板: ${entry.key[1]} (过期时间: ${clipboard.expires_at})`);
      }
    }
    
    console.log(`✅ 过期清理完成: 检查了 ${totalCount} 个剪贴板,删除了 ${cleanedCount} 个过期剪贴板`);
  } catch (error) {
    console.error("❌ 过期清理过程中出错:", error);
  }
}

// 启动定时清理任务
function startCleanupTasks() {
  // 每小时清理一次空内容剪贴板
  setInterval(async () => {
    await cleanupEmptyClipboards();
  }, 60 * 60 * 1000); // 1小时

  // 每30分钟清理一次过期剪贴板
  setInterval(async () => {
    await cleanupExpiredClipboards();
  }, 30 * 60 * 1000); // 30分钟

  // 启动时立即执行一次清理
  setTimeout(async () => {
    await cleanupExpiredClipboards();
    await cleanupEmptyClipboards();
  }, 5000); // 启动5秒后执行

  console.log("🔄 自动清理任务已启动");
  console.log("  - 空内容清理: 每小时执行一次");
  console.log("  - 过期内容清理: 每30分钟执行一次");
}

// 配置页面
function getConfigPage(): string {
  const currentUrl = Deno.env.get("DENO_DEPLOYMENT_ID") 
    ? `https://${Deno.env.get("DENO_DEPLOYMENT_ID")}.deno.dev`
    : "https://your-deploy-url.deno.dev";

  return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>配置 OAuth2 - 在线剪贴板编辑器</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        .config-container {
            max-width: 800px;
            margin: 0 auto;
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(15px);
            border-radius: 20px;
            padding: 40px;
            box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
        }
        .logo { font-size: 3rem; text-align: center; margin-bottom: 20px; }
        h1 {
            color: #333;
            font-size: 2rem;
            font-weight: 700;
            margin-bottom: 10px;
            text-align: center;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }
        .subtitle {
            color: #666;
            text-align: center;
            margin-bottom: 40px;
            font-size: 1.1rem;
        }
        .status {
            background: #fff3cd;
            border: 2px solid #ffc107;
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 30px;
            color: #856404;
        }
        .status h3 {
            margin-bottom: 10px;
            color: #856404;
        }
        .env-vars {
            background: #f8f9fa;
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 30px;
        }
        .env-vars h3 {
            color: #333;
            margin-bottom: 15px;
        }
        .env-var {
            background: #1e1e1e;
            color: #d4d4d4;
            padding: 10px 15px;
            border-radius: 8px;
            font-family: 'Consolas', monospace;
            margin-bottom: 10px;
            font-size: 0.9rem;
            word-break: break-all;
        }
        .instructions {
            background: #e3f2fd;
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 30px;
        }
        .instructions h3 {
            color: #1976d2;
            margin-bottom: 15px;
        }
        .instructions ol {
            color: #333;
            padding-left: 20px;
        }
        .instructions li {
            margin-bottom: 10px;
            line-height: 1.6;
        }
        .current-config {
            background: #f1f3f4;
            border-radius: 12px;
            padding: 20px;
        }
        .current-config h3 {
            color: #333;
            margin-bottom: 15px;
        }
        .config-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 8px 0;
            border-bottom: 1px solid #ddd;
        }
        .config-item:last-child {
            border-bottom: none;
        }
        .config-label {
            font-weight: 600;
            color: #555;
        }
        .config-value {
            font-family: monospace;
            background: #e8f5e8;
            padding: 4px 8px;
            border-radius: 4px;
            color: #2e7d32;
        }
        .config-value.missing {
            background: #ffebee;
            color: #c62828;
        }
        .refresh-btn {
            display: block;
            width: 200px;
            margin: 30px auto 0;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            text-decoration: none;
            padding: 15px 30px;
            border-radius: 12px;
            font-size: 1.1rem;
            font-weight: 600;
            text-align: center;
            transition: all 0.3s ease;
            box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
        }
        .refresh-btn:hover {
            transform: translateY(-3px);
            box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4);
        }
    </style>
</head>
<body>
    <div class="config-container">
        <div class="logo">⚙️</div>
        <h1>OAuth2 配置</h1>
        <p class="subtitle">在线剪贴板编辑器需要配置 OAuth2 才能正常使用</p>
        
        <div class="status">
            <h3>🔧 配置状态</h3>
            <p>系统检测到 OAuth2 配置不完整,请按照以下步骤完成配置。</p>
        </div>

        <div class="env-vars">
            <h3>📋 需要设置的环境变量</h3>
            <div class="env-var">OAUTH2_BASE_URL=${currentUrl.replace('.deno.dev', '')}</div>
            <div class="env-var">OAUTH2_CLIENT_ID=your_oauth_client_id</div>
            <div class="env-var">OAUTH2_CLIENT_SECRET=your_oauth_client_secret</div>
            <div class="env-var">OAUTH2_REDIRECT_URI=${currentUrl}/auth/callback</div>
            <div class="env-var">JWT_SECRET=your_random_jwt_secret</div>
        </div>

        <div class="instructions">
            <h3>🚀 配置步骤</h3>
            <ol>
                <li><strong>在 Deno Deploy 项目设置中</strong>,找到 "Environment Variables" 部分</li>
                <li><strong>添加上述环境变量</strong>,将示例值替换为你的实际 OAuth2 配置</li>
                <li><strong>OAUTH2_BASE_URL</strong>:你的 OAuth2 提供商的基础 URL</li>
                <li><strong>OAUTH2_CLIENT_ID</strong>:OAuth2 应用的客户端 ID</li>
                <li><strong>OAUTH2_CLIENT_SECRET</strong>:OAuth2 应用的客户端密钥</li>
                <li><strong>OAUTH2_REDIRECT_URI</strong>:OAuth2 回调地址(通常是 ${currentUrl}/auth/callback)</li>
                <li><strong>JWT_SECRET</strong>:用于签名 JWT 的随机字符串</li>
                <li><strong>保存配置后</strong>,项目会自动重新部署</li>
                <li><strong>部署完成后</strong>,点击下方按钮刷新页面</li>
            </ol>
        </div>

        <div class="current-config">
            <h3>📊 当前配置状态</h3>
            <div class="config-item">
                <span class="config-label">OAUTH2_BASE_URL:</span>
                <span class="config-value ${OAUTH2_BASE_URL ? '' : 'missing'}">
                    ${OAUTH2_BASE_URL || '未设置'}
                </span>
            </div>
            <div class="config-item">
                <span class="config-label">OAUTH2_CLIENT_ID:</span>
                <span class="config-value ${OAUTH2_CLIENT_ID ? '' : 'missing'}">
                    ${OAUTH2_CLIENT_ID || '未设置'}
                </span>
            </div>
            <div class="config-item">
                <span class="config-label">OAUTH2_CLIENT_SECRET:</span>
                <span class="config-value ${OAUTH2_CLIENT_SECRET ? '' : 'missing'}">
                    ${OAUTH2_CLIENT_SECRET ? '已设置' : '未设置'}
                </span>
            </div>
            <div class="config-item">
                <span class="config-label">OAUTH2_REDIRECT_URI:</span>
                <span class="config-value ${OAUTH2_REDIRECT_URI ? '' : 'missing'}">
                    ${OAUTH2_REDIRECT_URI || '未设置'}
                </span>
            </div>
            <div class="config-item">
                <span class="config-label">JWT_SECRET:</span>
                <span class="config-value">已设置</span>
            </div>
        </div>

        <a href="/" class="refresh-btn">🔄 刷新页面</a>
    </div>
</body>
</html>`;
}

// OAuth2 函数
function getAuthorizationUrl(): string {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: OAUTH2_CLIENT_ID!,
    redirect_uri: OAUTH2_REDIRECT_URI!,
    state: Math.random().toString(36)
    //scope: 'read'
  });
  return `${OAUTH2_BASE_URL}/oauth2/auth?${params.toString()}`;
}

async function exchangeCodeForToken(code: string): Promise<string | null> {
  try {
    const response = await fetch(`${OAUTH2_BASE_URL}/oauth2/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: OAUTH2_CLIENT_ID!,
        client_secret: OAUTH2_CLIENT_SECRET!,
        code: code,
        redirect_uri: OAUTH2_REDIRECT_URI!
      })
    });
    const data = await response.json();
    return data.access_token;
  } catch {
    return null;
  }
}

async function getUserInfo(accessToken: string): Promise<any> {
  try {
    const response = await fetch(`${OAUTH2_BASE_URL}/oauth2/userinfo`, {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    });
    return await response.json();
  } catch {
    return null;
  }
}

// JWT 函数
async function generateJWT(payload: any): Promise<string> {
  const header = { alg: "HS256", typ: "JWT" };
  const encodedHeader = encodeBase64(new TextEncoder().encode(JSON.stringify(header)));
  const encodedPayload = encodeBase64(new TextEncoder().encode(JSON.stringify(payload)));
  
  const signature = await crypto.subtle.sign(
    "HMAC",
    await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(JWT_SECRET!),
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign"]
    ),
    new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`)
  );
  
  const encodedSignature = encodeBase64(new Uint8Array(signature));
  return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
}

async function verifyJWT(token: string): Promise<any> {
  try {
    const [header, payload, signature] = token.split('.');
    const expectedSignature = await crypto.subtle.sign(
      "HMAC",
      await crypto.subtle.importKey(
        "raw",
        new TextEncoder().encode(JWT_SECRET!),
        { name: "HMAC", hash: "SHA-256" },
        false,
        ["sign"]
      ),
      new TextEncoder().encode(`${header}.${payload}`)
    );
    
    const expectedEncodedSignature = encodeBase64(new Uint8Array(expectedSignature));
    if (signature !== expectedEncodedSignature) throw new Error("Invalid");
    
    return JSON.parse(new TextDecoder().decode(decodeBase64(payload)));
  } catch {
    throw new Error("Invalid token");
  }
}

// 用户认证
async function createUserSession(userInfo: any): Promise<string> {
  const sessionToken = await generateJWT({
    userId: userInfo.id,
    username: userInfo.sub,
    email: userInfo.email,
    avatar: userInfo.avatar,
    loginTime: Date.now()
  });
  
  userSessions.set(sessionToken, {
    id: userInfo.id,
    username: userInfo.sub,
    email: userInfo.email,
    avatar: userInfo.avatar
  });
  
  return sessionToken;
}

async function verifyUserSession(request: Request): Promise<any> {
  const authHeader = request.headers.get('Authorization');
  const cookieHeader = request.headers.get('Cookie');
  
  let token = null;
  if (authHeader?.startsWith('Bearer ')) {
    token = authHeader.substring(7);
  } else if (cookieHeader) {
    const match = cookieHeader.match(/auth_token=([^;]+)/);
    if (match) token = match[1];
  }
  
  if (!token) return null;
  
  try {
    const payload = await verifyJWT(token);
    const userSession = userSessions.get(token);
    if (userSession && payload.userId === userSession.id) {
      return userSession;
    }
    return null;
  } catch {
    return null;
  }
}

// 加密函数
async function encryptText(text: string, password: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(text);
  const passwordData = encoder.encode(password);
  
  const key = await crypto.subtle.importKey(
    "raw",
    await crypto.subtle.digest("SHA-256", passwordData),
    { name: "AES-GCM" },
    false,
    ["encrypt"]
  );
  
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
  
  const combined = new Uint8Array(iv.length + encrypted.byteLength);
  combined.set(iv);
  combined.set(new Uint8Array(encrypted), iv.length);
  
  return encodeBase64(combined);
}

async function decryptText(encryptedText: string, password: string): Promise<string> {
  const encoder = new TextEncoder();
  const decoder = new TextDecoder();
  const passwordData = encoder.encode(password);
  
  const key = await crypto.subtle.importKey(
    "raw",
    await crypto.subtle.digest("SHA-256", passwordData),
    { name: "AES-GCM" },
    false,
    ["decrypt"]
  );
  
  const combined = decodeBase64(encryptedText);
  const iv = combined.slice(0, 12);
  const encrypted = combined.slice(12);
  
  const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encrypted);
  return decoder.decode(decrypted);
}

async function hashPassword(password: string): Promise<string> {
  const data = new TextEncoder().encode(password);
  const hash = await crypto.subtle.digest("SHA-256", data);
  return encodeBase64(new Uint8Array(hash));
}

// 工具函数
function generateShareCode(): string {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < 8; i++) {
    result += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return result;
}

// 登录页面
function getLoginPage(): string {
  const authUrl = getAuthorizationUrl();
  return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登录 - 在线剪贴板编辑器</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .login-container {
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(15px);
            border-radius: 20px;
            padding: 40px;
            box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
            text-align: center;
            max-width: 400px;
            width: 90%;
        }
        .logo { font-size: 3rem; margin-bottom: 20px; }
        h1 {
            color: #333;
            font-size: 1.8rem;
            font-weight: 700;
            margin-bottom: 10px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }
        .subtitle { color: #666; margin-bottom: 30px; }
        .login-btn {
            display: inline-block;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            text-decoration: none;
            padding: 15px 30px;
            border-radius: 12px;
            font-size: 1.1rem;
            font-weight: 600;
            transition: all 0.3s ease;
            box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
        }
        .login-btn:hover {
            transform: translateY(-3px);
            box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4);
        }
    </style>
</head>
<body>
    <div class="login-container">
        <div class="logo">📝</div>
        <h1>在线剪贴板编辑器</h1>
        <p class="subtitle">安全、便捷的代码分享平台</p>
        <a href="${authUrl}" class="login-btn">🔐 OAuth2 登录</a>
    </div>
</body>
</html>`;
}

// 主页面
function getHomePage(shareCode: string, userInfo: any, clipboardExists: boolean, isEncrypted: boolean): string {
  return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在线剪贴板编辑器</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            height: 100vh;
            overflow: hidden;
        }
        .app-container {
            display: flex;
            height: 100vh;
        }
        .editor-section {
            flex: 1;
            display: flex;
            flex-direction: column;
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(15px);
        }
        .header {
            padding: 20px;
            background: rgba(255, 255, 255, 0.9);
            border-bottom: 1px solid rgba(0, 0, 0, 0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .header h1 {
            color: #333;
            font-size: 1.8rem;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }
        .user-info {
            display: flex;
            align-items: center;
            gap: 12px;
            background: rgba(255, 255, 255, 0.8);
            padding: 8px 16px;
            border-radius: 12px;
        }
        .user-avatar {
            width: 32px;
            height: 32px;
            border-radius: 50%;
            border: 2px solid #667eea;
        }
        .user-details {
            display: flex;
            flex-direction: column;
        }
        .user-name { font-weight: 600; color: #333; font-size: 0.9rem; }
        .user-email { font-size: 0.75rem; color: #666; }
        .logout-btn {
            background: #ff6b6b;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 8px;
            cursor: pointer;
            margin-left: 12px;
        }
        .share-info {
            text-align: center;
            margin: 10px 0;
        }
        .share-code {
            font-family: monospace;
            background: #e3f2fd;
            padding: 8px 12px;
            border-radius: 8px;
            color: #1976d2;
            font-weight: bold;
            margin-bottom: 10px;
            position: relative;
        }
        .share-code.encrypted::after {
            content: '🔒';
            position: absolute;
            right: 8px;
        }
        .actions {
            display: flex;
            gap: 10px;
            justify-content: center;
            flex-wrap: wrap;
            align-items: center;
        }
        .btn {
            padding: 8px 16px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 0.8rem;
            font-weight: 600;
        }
        .btn-primary { background: #667eea; color: white; }
        .btn-success { background: #4caf50; color: white; }
        .btn-secondary { background: #f8f9fa; color: #555; border: 1px solid #ddd; }
        .expire-info {
            background: rgba(255, 193, 7, 0.1);
            border: 1px solid #ffc107;
            border-radius: 6px;
            padding: 4px 8px;
            font-size: 0.75rem;
            color: #856404;
            margin-left: 8px;
            white-space: nowrap;
            display: inline-flex;
            align-items: center;
            gap: 4px;
        }
        .expire-info.expired {
            background: rgba(244, 67, 54, 0.1);
            border-color: #f44336;
            color: #c62828;
        }
        .expire-info.permanent {
            background: rgba(76, 175, 80, 0.1);
            border-color: #4caf50;
            color: #2e7d32;
        }
        .expire-info.loading {
            background: rgba(158, 158, 158, 0.1);
            border-color: #9e9e9e;
            color: #616161;
        }
        .encrypted-notice {
            background: #fff3cd;
            border: 2px solid #ffc107;
            padding: 15px;
            margin: 10px 20px;
            border-radius: 8px;
            color: #856404;
            text-align: center;
            display: none;
        }
        .encrypted-notice.show { display: block; }
        .editor-content {
            flex: 1;
            display: flex;
            flex-direction: column;
        }
        .editor-wrapper {
            flex: 1;
            display: flex;
            position: relative;
            background: #1e1e1e;
        }
        .line-numbers {
            background: #2d2d2d;
            color: #858585;
            padding: 20px 10px 20px 20px;
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
            font-size: 14px;
            line-height: 1.5;
            text-align: right;
            user-select: none;
            border-right: 1px solid #404040;
            min-width: 60px;
            overflow: hidden;
            white-space: pre-line;
        }
        .code-editor {
            flex: 1;
            background: #1e1e1e;
            color: #d4d4d4;
            border: none;
            outline: none;
            padding: 20px;
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
            font-size: 14px;
            line-height: 1.5;
            resize: none;
            white-space: pre;
            overflow-wrap: normal;
            overflow-x: auto;
        }
        .control-panel {
            width: 350px;
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(15px);
            display: flex;
            flex-direction: column;
            border-left: 1px solid rgba(0, 0, 0, 0.1);
        }
        .panel-header {
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }
        .panel-content {
            flex: 1;
            padding: 20px;
            overflow-y: auto;
        }
        .control-group {
            margin-bottom: 25px;
            background: rgba(255, 255, 255, 0.8);
            padding: 15px;
            border-radius: 12px;
        }
        .control-group h3 {
            color: #333;
            margin-bottom: 15px;
            font-size: 1.1rem;
        }
        .input-group {
            margin-bottom: 15px;
        }
        .input-group label {
            display: block;
            color: #555;
            font-size: 0.9rem;
            font-weight: 600;
            margin-bottom: 5px;
        }
        .input-group input, .input-group select {
            width: 100%;
            padding: 10px;
            border: 2px solid #e1e5e9;
            border-radius: 8px;
            font-size: 0.9rem;
        }
        .input-group input:focus, .input-group select:focus {
            outline: none;
            border-color: #667eea;
        }
        .button-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 10px;
        }
        .password-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 1000;
        }
        .password-modal-content {
            background: white;
            border-radius: 12px;
            padding: 30px;
            width: 90%;
            max-width: 400px;
            text-align: center;
        }
        .password-modal h3 {
            color: #333;
            margin-bottom: 20px;
        }
        .password-modal input {
            width: 100%;
            padding: 12px;
            border: 2px solid #e1e5e9;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        .password-modal-buttons {
            display: flex;
            gap: 10px;
            justify-content: center;
        }
        .password-error {
            color: #dc3545;
            font-size: 0.8rem;
            margin-top: 10px;
            display: none;
        }
        .save-indicator {
            position: fixed;
            top: 20px;
            right: 20px;
            background: rgba(76, 175, 80, 0.9);
            color: white;
            padding: 8px 16px;
            border-radius: 20px;
            font-size: 0.8rem;
            font-weight: 600;
            opacity: 0;
            transition: opacity 0.3s ease;
            z-index: 1000;
        }
        .save-indicator.show {
            opacity: 1;
        }
        .save-indicator.saving {
            background: rgba(255, 193, 7, 0.9);
        }
        .save-indicator.error {
            background: rgba(244, 67, 54, 0.9);
        }
        .password-status {
            background: #e8f5e8;
            border: 1px solid #4caf50;
            border-radius: 8px;
            padding: 10px;
            margin-bottom: 15px;
            color: #2e7d32;
            font-size: 0.85rem;
            display: none;
        }
        .password-status.show {
            display: block;
        }
        .clear-password-btn {
            background: #ff9800;
            color: white;
            border: none;
            padding: 6px 12px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 0.75rem;
            margin-left: 10px;
        }
        
        /* LayUI 风格的消息提示框 */
        .layui-message {
            position: fixed;
            top: 50px;
            left: 50%;
            transform: translateX(-50%);
            z-index: 9999;
            background: #fff;
            border-radius: 6px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            padding: 12px 20px;
            font-size: 14px;
            color: #333;
            opacity: 0;
            transition: all 0.3s ease;
            pointer-events: none;
            min-width: 200px;
            text-align: center;
        }
        
        .layui-message.show {
            opacity: 1;
            transform: translateX(-50%) translateY(10px);
        }
        
        .layui-message.success {
            border-left: 4px solid #5fb878;
        }
        
        .layui-message.info {
            border-left: 4px solid #1e9fff;
        }
        
        .layui-message.warning {
            border-left: 4px solid #ffb800;
        }
        
        .layui-message.error {
            border-left: 4px solid #ff5722;
        }
        
        .layui-message-icon {
            display: inline-block;
            margin-right: 8px;
            font-size: 16px;
        }
        
        @media (max-width: 768px) {
            .app-container { flex-direction: column; }
            .control-panel { width: 100%; height: 40vh; }
            .editor-section { height: 60vh; }
            .header { flex-direction: column; gap: 10px; text-align: center; }
            .actions { justify-content: center; }
            .line-numbers { min-width: 40px; padding: 20px 5px 20px 10px; }
        }
    </style>
</head>
<body>
    <div class="save-indicator" id="saveIndicator">💾 已保存</div>
    
    <div class="app-container">
        <div class="editor-section">
            <div class="header">
                <h1>📝 在线剪贴板编辑器</h1>
                <div style="display: flex; align-items: center;">
                    <div class="user-info">
                        <img src="${userInfo.avatar}" alt="${userInfo.username}" class="user-avatar">
                        <div class="user-details">
                            <div class="user-name">${userInfo.username}</div>
                            <div class="user-email">${userInfo.email}</div>
                        </div>
                    </div>
                    <button class="logout-btn" onclick="logout()">退出</button>
                </div>
            </div>
            
            <div class="share-info">
                <div class="share-code ${isEncrypted ? 'encrypted' : ''}" id="shareCode">${shareCode}</div>
                <div class="actions">
                    <button class="btn btn-primary" onclick="createNew()">➕ 新建</button>
                    <button class="btn btn-success" onclick="copyLink()">🔗 复制链接</button>
                    <span class="expire-info loading" id="expireInfo">⏳ 加载中...</span>
                </div>
            </div>
            
            <div class="encrypted-notice" id="encryptedNotice">
                🔒 此剪贴板已加密,需要输入正确密码才能查看内容
            </div>
            
            <div class="editor-content">
                <div class="editor-wrapper">
                    <div class="line-numbers" id="lineNumbers">1</div>
                    <textarea class="code-editor" id="codeEditor" placeholder="在这里输入你的代码...

支持快捷键:
• Ctrl + S:保存内容
• Ctrl + Enter:复制到剪贴板
• Ctrl + L:清空编辑器

内容会自动保存到云端,可通过分享链接访问"></textarea>
                </div>
            </div>
        </div>

        <div class="control-panel">
            <div class="panel-header">
                <h2>🎛️ 控制面板</h2>
            </div>
            <div class="panel-content">
                <div class="control-group">
                    <h3>🔐 安全设置</h3>
                    <div class="password-status" id="passwordStatus">
                        🔓 密码已验证,本次会话有效
                        <button class="clear-password-btn" onclick="clearStoredPassword()">清除</button>
                    </div>
                    <div class="input-group">
                        <label>加密密码(可选)</label>
                        <input type="password" id="passwordInput" placeholder="设置密码保护内容">
                    </div>
                    <div class="input-group">
                        <label>过期时间</label>
                        <select id="expireSelect">
                            <option value="24" selected>1天后过期(默认)</option>
                            <option value="">永不过期</option>
                            <option value="1">1小时后过期</option>
                            <option value="12">12小时后过期</option>
                            <option value="168">1周后过期</option>
                            <option value="720">1个月后过期</option>
                        </select>
                    </div>
                </div>

                <div class="control-group">
                    <h3>🚀 快速操作</h3>
                    <div class="button-grid">
                        <button class="btn btn-primary" onclick="copyToClipboard()">📄 复制</button>
                        <button class="btn btn-secondary" onclick="pasteFromClipboard()">📋 粘贴</button>
                        <button class="btn btn-success" onclick="saveContent()">💾 保存</button>
                        <button class="btn btn-secondary" onclick="clearEditor()">🗑️ 清空</button>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="password-modal" id="passwordModal">
        <div class="password-modal-content">
            <h3>🔒 输入密码</h3>
            <p>此剪贴板已加密,请输入密码以查看内容</p>
            <input type="password" id="modalPasswordInput" placeholder="请输入密码">
            <div class="password-error" id="passwordError">密码错误,请重试</div>
            <div class="password-modal-buttons">
                <button class="btn btn-primary" onclick="submitPassword()">确认</button>
                <button class="btn btn-secondary" onclick="closePasswordModal()">取消</button>
            </div>
        </div>
    </div>

    <script>
        let shareCode = '${shareCode}';
        let isEncrypted = ${isEncrypted};
        let clipboardExists = ${clipboardExists};
        let saveTimeout;
        let isSaving = false;
        let currentExpireTime = null; // 添加全局变量存储当前过期时间

        // LayUI 风格的消息提示函数
        function showLayuiMessage(content, type = 'success', duration = 2000) {
            // 移除已存在的消息框
            const existingMessage = document.querySelector('.layui-message');
            if (existingMessage) {
                existingMessage.remove();
            }
            
            // 创建消息框
            const messageBox = document.createElement('div');
            messageBox.className = \`layui-message \${type}\`;
            
            // 设置图标
            let icon = '';
            switch(type) {
                case 'success':
                    icon = '✓';
                    break;
                case 'info':
                    icon = 'ℹ';
                    break;
                case 'warning':
                    icon = '⚠';
                    break;
                case 'error':
                    icon = '✗';
                    break;
                default:
                    icon = 'ℹ';
            }
            
            messageBox.innerHTML = \`
                <span class="layui-message-icon">\${icon}</span>
                <span>\${content}</span>
            \`;
            
            // 添加到页面
            document.body.appendChild(messageBox);
            
            // 显示动画
            setTimeout(() => {
                messageBox.classList.add('show');
            }, 10);
            
            // 自动隐藏
            setTimeout(() => {
                messageBox.classList.remove('show');
                setTimeout(() => {
                    if (messageBox.parentNode) {
                        messageBox.parentNode.removeChild(messageBox);
                    }
                }, 300);
            }, duration);
        }

        // 密码存储相关函数
        function getStoredPassword() {
            return sessionStorage.getItem('clipboard_password_' + shareCode);
        }

        function storePassword(password) {
            sessionStorage.setItem('clipboard_password_' + shareCode, password);
            updatePasswordStatus();
        }

        function clearStoredPassword() {
            sessionStorage.removeItem('clipboard_password_' + shareCode);
            document.getElementById('passwordInput').value = '';
            updatePasswordStatus();
            showSaveIndicator('success', '🔓 密码已清除');
        }

        function updatePasswordStatus() {
            const storedPassword = getStoredPassword();
            const passwordStatus = document.getElementById('passwordStatus');
            
            if (storedPassword && isEncrypted) {
                passwordStatus.classList.add('show');
                // 自动填充密码输入框
                document.getElementById('passwordInput').value = storedPassword;
            } else {
                passwordStatus.classList.remove('show');
            }
        }

        function getAuthToken() {
            const cookies = document.cookie.split(';');
            for (const cookie of cookies) {
                const [name, value] = cookie.trim().split('=');
                if (name === 'auth_token') return value;
            }
            return '';
        }

        // 格式化过期时间显示
        function formatExpireTime(expiresAt) {
            if (!expiresAt) {
                return { text: '♾️ 永不过期', type: 'permanent' };
            }
            
            const expireDate = new Date(expiresAt);
            const now = new Date();
            const diffMs = expireDate.getTime() - now.getTime();
            
            if (diffMs <= 0) {
                return { text: '⚠️ 已过期', type: 'expired' };
            }
            
            const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
            const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
            
            let timeText = '';
            if (diffDays > 1) {
                timeText = \`\${diffDays}天后过期\`;
            } else if (diffHours > 1) {
                timeText = \`\${diffHours}小时后过期\`;
            } else {
                const diffMinutes = Math.ceil(diffMs / (1000 * 60));
                timeText = \`\${diffMinutes}分钟后过期\`;
            }
            
            return { text: \`⏰ \${timeText}\`, type: 'normal' };
        }

        // 更新过期时间显示
        function updateExpireDisplay(expiresAt = null) {
            const expireInfo = document.getElementById('expireInfo');
            if (!expireInfo) return;
            
            // 如果传入了新的过期时间,更新全局变量
            if (expiresAt !== null) {
                currentExpireTime = expiresAt;
            }
            
            const { text, type } = formatExpireTime(currentExpireTime);
            
            expireInfo.textContent = text;
            expireInfo.className = \`expire-info \${type}\`;
        }

        // 获取剪贴板信息(包括过期时间)
        async function getClipboardInfo() {
            try {
                const response = await fetch('/api/info', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': 'Bearer ' + getAuthToken()
                    },
                    body: JSON.stringify({ shareCode })
                });

                const result = await response.json();
                if (result.success) {
                    currentExpireTime = result.expires_at;
                    updateExpireDisplay();
                    
                    // 如果剪贴板已过期,重定向到新的剪贴板
                    if (result.expired) {
                        const newCode = Math.random().toString(36).substr(2, 8);
                        window.location.href = '/s/' + newCode;
                    }
                } else {
                    // 如果获取失败,显示未知状态
                    document.getElementById('expireInfo').textContent = '❓ 未知';
                    document.getElementById('expireInfo').className = 'expire-info loading';
                }
            } catch (error) {
                console.error('获取剪贴板信息失败:', error);
                document.getElementById('expireInfo').textContent = '❌ 获取失败';
                document.getElementById('expireInfo').className = 'expire-info expired';
            }
        }

        function updateLineNumbers() {
            const editor = document.getElementById('codeEditor');
            const lineNumbers = document.getElementById('lineNumbers');
            const content = editor.value;
            const lines = content.split('\\n');
            const lineCount = lines.length;
            
            const lineNumberArray = [];
            for (let i = 1; i <= lineCount; i++) {
                lineNumberArray.push(i.toString());
            }
            
            lineNumbers.innerHTML = lineNumberArray.join('<br>');
            lineNumbers.scrollTop = editor.scrollTop;
        }

        function syncScroll() {
            const editor = document.getElementById('codeEditor');
            const lineNumbers = document.getElementById('lineNumbers');
            lineNumbers.scrollTop = editor.scrollTop;
        }

        function showSaveIndicator(type = 'success', message = '💾 已保存') {
            const indicator = document.getElementById('saveIndicator');
            indicator.textContent = message;
            indicator.className = 'save-indicator show';
            
            if (type === 'saving') {
                indicator.classList.add('saving');
            } else if (type === 'error') {
                indicator.classList.add('error');
            }
            
            setTimeout(() => {
                indicator.classList.remove('show');
                setTimeout(() => {
                    indicator.className = 'save-indicator';
                }, 300);
            }, 2000);
        }

        function logout() {
            if (confirm('确定要退出登录吗?')) {
                const keys = Object.keys(sessionStorage);
                keys.forEach(key => {
                    if (key.startsWith('clipboard_password_')) {
                        sessionStorage.removeItem(key);
                    }
                });
                
                document.cookie = 'auth_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
                window.location.href = '/login';
            }
        }

        async function createNew() {
            const newCode = Math.random().toString(36).substr(2, 8);
            window.location.href = '/s/' + newCode;
        }

        function copyLink() {
            const link = window.location.origin + '/s/' + shareCode;
            navigator.clipboard.writeText(link).then(() => {
                showSaveIndicator('success', '🔗 链接已复制');
            });
        }

        // 修复后的 saveContent 函数
        async function saveContent(showIndicator = false) {
            if (isSaving) return;
            
            const content = document.getElementById('codeEditor').value;
            let password = document.getElementById('passwordInput').value;
            const expireSelect = document.getElementById('expireSelect');
            const expireHours = expireSelect.value;

            // 如果没有输入密码但有存储的密码,使用存储的密码
            if (!password && isEncrypted) {
                password = getStoredPassword();
            }

            if (showIndicator) {
                showSaveIndicator('saving', '💾 保存中...');
            }

            isSaving = true;

            try {
                // 构建请求数据
                const requestData = {
                    shareCode,
                    content,
                    password,
                    expireHours: expireHours ? parseFloat(expireHours) : null
                };

                console.log('保存数据:', requestData); // 调试日志

                const response = await fetch('/api/save', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': 'Bearer ' + getAuthToken()
                    },
                    body: JSON.stringify(requestData)
                });

                const result = await response.json();
                console.log('保存结果:', result); // 调试日志

                if (result.success) {
                    // 更新过期时间显示
                    updateExpireDisplay(result.expires_at);
                    
                    if (showIndicator) {
                        const expireText = expireHours ? 
                            (expireHours == 1 ? '1小时' : 
                             expireHours == 12 ? '12小时' : 
                             expireHours == 24 ? '1天' : 
                             expireHours == 168 ? '1周' : 
                             expireHours == 720 ? '1个月' : expireHours + '小时') : 
                            '永不';
                        showSaveIndicator('success', \`💾 保存成功 (过期: \${expireText})\`);
                    }
                    
                    if (password && !isEncrypted) {
                        isEncrypted = true;
                        document.getElementById('shareCode').classList.add('encrypted');
                        storePassword(password);
                    }
                } else {
                    if (showIndicator) {
                        showSaveIndicator('error', '❌ 保存失败: ' + (result.error || '未知错误'));
                    }
                    console.error('保存失败:', result.error);
                }
            } catch (error) {
                console.error('保存错误:', error);
                if (showIndicator) {
                    showSaveIndicator('error', '❌ 网络错误');
                }
            } finally {
                isSaving = false;
            }
        }

        async function loadContent() {
            let password = document.getElementById('passwordInput').value;
            
            if (!password && isEncrypted) {
                password = getStoredPassword();
            }
            
            try {
                const response = await fetch('/api/load', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': 'Bearer ' + getAuthToken()
                    },
                    body: JSON.stringify({ shareCode, password })
                });

                const result = await response.json();
                if (result.success) {
                    document.getElementById('codeEditor').value = result.content;
                    updateLineNumbers();
                    
                    if (password && isEncrypted) {
                        storePassword(password);
                    }
                    
                    return true;
                } else if (result.error === '需要密码' || result.error === '密码错误') {
                    return false;
                }
            } catch (error) {
                console.error('加载错误:', error);
            }
            return false;
        }

        function showPasswordModal() {
            document.getElementById('passwordModal').style.display = 'flex';
            document.getElementById('modalPasswordInput').focus();
        }

        function closePasswordModal() {
            document.getElementById('passwordModal').style.display = 'none';
        }

        async function submitPassword() {
            const password = document.getElementById('modalPasswordInput').value;
            if (!password) return;

            document.getElementById('passwordInput').value = password;
            const success = await loadContent();
            
            if (success) {
                closePasswordModal();
                document.getElementById('encryptedNotice').classList.remove('show');
                storePassword(password);
                showSaveIndicator('success', '🔓 密码验证成功');
            } else {
                document.getElementById('passwordError').style.display = 'block';
            }
        }

        async function copyToClipboard() {
            const text = document.getElementById('codeEditor').value;
            if (!text.trim()) {
                showSaveIndicator('error', '❌ 内容为空');
                return;
            }
            try {
                await navigator.clipboard.writeText(text);
                showSaveIndicator('success', '📄 已复制');
            } catch {
                showSaveIndicator('error', '❌ 复制失败');
            }
        }

        async function pasteFromClipboard() {
            try {
                const text = await navigator.clipboard.readText();
                const editor = document.getElementById('codeEditor');
                const start = editor.selectionStart;
                const end = editor.selectionEnd;
                const currentValue = editor.value;
                
                editor.value = currentValue.substring(0, start) + text + currentValue.substring(end);
                editor.selectionStart = editor.selectionEnd = start + text.length;
                
                updateLineNumbers();
                showSaveIndicator('success', '📋 已粘贴');
            } catch {
                showSaveIndicator('error', '❌ 粘贴失败');
            }
        }

        function clearEditor() {
            if (confirm('确定要清空编辑器内容吗?')) {
                document.getElementById('codeEditor').value = '';
                updateLineNumbers();
                showSaveIndicator('success', '🗑️ 已清空');
            }
        }

        // 初始化事件监听器
        document.addEventListener('DOMContentLoaded', () => {
            const editor = document.getElementById('codeEditor');
            
            editor.addEventListener('input', () => {
                updateLineNumbers();
                
                clearTimeout(saveTimeout);
                saveTimeout = setTimeout(() => {
                    if (editor.value.trim()) {
                        saveContent(false);
                    }
                }, 2000);
            });
            
            editor.addEventListener('scroll', syncScroll);
            
            editor.addEventListener('keydown', (e) => {
                if (e.key === 'Tab') {
                    e.preventDefault();
                    const start = editor.selectionStart;
                    const end = editor.selectionEnd;
                    const value = editor.value;
                    
                    editor.value = value.substring(0, start) + '\\t' + value.substring(end);
                    editor.selectionStart = editor.selectionEnd = start + 1;
                    updateLineNumbers();
                }
            });
            
            updateLineNumbers();
            updatePasswordStatus();
        });

        // 快捷键
        document.addEventListener('keydown', (e) => {
            if (e.ctrlKey || e.metaKey) {
                switch(e.key) {
                    case 's':
                        e.preventDefault();
                        saveContent(true);
                        break;
                    case 'Enter':
                        e.preventDefault();
                        copyToClipboard();
                        break;
                    case 'l':
                        e.preventDefault();
                        clearEditor();
                        break;
                }
            }
        });

        // 监听过期时间选择变化 - 添加 LayUI 提示
        document.getElementById('expireSelect').addEventListener('change', async (e) => {
            const expireHours = e.target.value;
            const expireText = expireHours ? 
                (expireHours == 1 ? '1小时' : 
                 expireHours == 12 ? '12小时' : 
                 expireHours == 24 ? '1天' : 
                 expireHours == 168 ? '1周' : 
                 expireHours == 720 ? '1个月' : expireHours + '小时') : 
                '永不过期';
            
            console.log('过期时间已更改为:', expireText);
            
            // 立即更新显示(预览效果)
            if (expireHours) {
                const futureTime = new Date(Date.now() + parseFloat(expireHours) * 60 * 60 * 1000).toISOString();
                updateExpireDisplay(futureTime);
            } else {
                updateExpireDisplay(null);
            }
            
            // 显示 LayUI 风格的提示
            showLayuiMessage(\`过期时间已设置为: \${expireText}\`, 'info', 2000);
            
            // 自动保存新的过期设置
            const content = document.getElementById('codeEditor').value;
            if (content.trim()) {
                // 延迟保存,让用户看到提示
                setTimeout(async () => {
                    await saveContent(false);
                    // 保存成功后再显示一个提示
                    showLayuiMessage('过期时间设置已保存', 'success', 1500);
                }, 500);
            } else {
                // 即使没有内容也要保存过期设置
                setTimeout(async () => {
                    await saveContent(false);
                    showLayuiMessage('过期时间设置已保存', 'success', 1500);
                }, 500);
            }
        });

        // 监听密码输入框变化
        document.getElementById('passwordInput').addEventListener('input', (e) => {
            const password = e.target.value;
            if (password && isEncrypted) {
                storePassword(password);
            }
        });

        // 初始化
        window.onload = async () => {
            const storedPassword = getStoredPassword();
            
            // 如果剪贴板存在,获取过期时间信息
            if (clipboardExists) {
                await getClipboardInfo();
            } else {
                // 新剪贴板,显示默认过期时间(1天)
                const defaultExpireTime = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
                updateExpireDisplay(defaultExpireTime);
            }
            
            if (isEncrypted && clipboardExists) {
                if (storedPassword) {
                    document.getElementById('passwordInput').value = storedPassword;
                    const success = await loadContent();
                    if (success) {
                        updatePasswordStatus();
                    } else {
                        clearStoredPassword();
                        document.getElementById('encryptedNotice').classList.add('show');
                        showPasswordModal();
                    }
                } else {
                    document.getElementById('encryptedNotice').classList.add('show');
                    showPasswordModal();
                }
            } else if (clipboardExists) {
                await loadContent();
            }
            
            // 每分钟更新一次过期时间显示
            setInterval(() => {
                if (currentExpireTime) {
                    updateExpireDisplay();
                }
            }, 60000); // 60秒
        };
    </script>
</body>
</html>`;
}

// API 处理
async function handleAPI(request: Request, path: string): Promise<Response> {
  const corsHeaders = {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  };

  try {
    // OAuth2 回调
    if (path === '/auth/callback') {
      const url = new URL(request.url);
      const code = url.searchParams.get('code');
      
      if (!code) {
        return new Response('No code provided', { status: 400 });
      }
      
      const accessToken = await exchangeCodeForToken(code);
      if (!accessToken) {
        return new Response('Failed to get token', { status: 400 });
      }
      
      const userInfo = await getUserInfo(accessToken);
      if (!userInfo) {
        return new Response('Failed to get user info', { status: 400 });
      }
      
      const sessionToken = await createUserSession(userInfo);
      
      return new Response(null, {
        status: 302,
        headers: {
          'Location': '/',
          'Set-Cookie': `auth_token=${sessionToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`
        }
      });
    }

    // 验证用户认证
    const userSession = await verifyUserSession(request);
    if (!userSession) {
      return new Response(JSON.stringify({ 
        success: false, 
        error: 'Authentication required' 
      }), { status: 401, headers: corsHeaders });
    }

    // 新增:获取剪贴板信息 API
    if (path === '/api/info') {
      const { shareCode } = await request.json();
      
      const result = await kv.get(['clipboards', shareCode]);
      const clipboard = result.value as any;

      if (!clipboard) {
        return new Response(JSON.stringify({ 
          success: false, 
          error: '剪贴板不存在' 
        }), { headers: corsHeaders });
      }

      // 检查是否过期
      const now = new Date();
      const isExpired = clipboard.expires_at && new Date(clipboard.expires_at) < now;
      
      if (isExpired) {
        await kv.delete(['clipboards', shareCode]);
        return new Response(JSON.stringify({ 
          success: true,
          expired: true,
          expires_at: clipboard.expires_at
        }), { headers: corsHeaders });
      }

      return new Response(JSON.stringify({ 
        success: true,
        expires_at: clipboard.expires_at,
        encrypted: clipboard.encrypted,
        has_content: clipboard.has_content,
        created_at: clipboard.created_at,
        updated_at: clipboard.updated_at,
        expired: false
      }), { headers: corsHeaders });
    }

    // 修复后的保存 API
    if (path === '/api/save') {
      const { shareCode, content, password, expireHours } = await request.json();
      
      console.log('接收到保存请求:', { 
        shareCode, 
        contentLength: content?.length, 
        hasPassword: !!password, 
        expireHours 
      }); // 调试日志
      
      let encryptedContent = content;
      let passwordHash = null;
      let encrypted = false;

      if (password) {
        encryptedContent = await encryptText(content, password);
        passwordHash = await hashPassword(password);
        encrypted = true;
      }

      let expiresAt = null;
      if (expireHours && expireHours > 0) {
        expiresAt = new Date(Date.now() + expireHours * 60 * 60 * 1000).toISOString();
        console.log('设置过期时间:', expiresAt, '(', expireHours, '小时后)'); // 调试日志
      } else {
        console.log('未设置过期时间或设置为永不过期'); // 调试日志
      }

      const clipboardData = {
        content: encryptedContent,
        encrypted,
        password_hash: passwordHash,
        expires_at: expiresAt,
        has_content: content.trim().length > 0,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString()
      };

      console.log('保存到数据库的数据:', clipboardData); // 调试日志

      await kv.set(['clipboards', shareCode], clipboardData);
      
      return new Response(JSON.stringify({ 
        success: true, 
        expires_at: expiresAt 
      }), { headers: corsHeaders });
    }

    if (path === '/api/load') {
      const { shareCode, password } = await request.json();
      
      const result = await kv.get(['clipboards', shareCode]);
      const clipboard = result.value as any;

      if (!clipboard) {
        return new Response(JSON.stringify({ 
          success: false, 
          error: '剪贴板不存在' 
        }), { headers: corsHeaders });
      }

      if (clipboard.expires_at && new Date(clipboard.expires_at) < new Date()) {
        await kv.delete(['clipboards', shareCode]);
        return new Response(JSON.stringify({ 
          success: false, 
          error: '剪贴板已过期' 
        }), { headers: corsHeaders });
      }

      if (clipboard.encrypted) {
        if (!password) {
          return new Response(JSON.stringify({ 
            success: false, 
            error: '需要密码' 
          }), { headers: corsHeaders });
        }

        const inputPasswordHash = await hashPassword(password);
        if (inputPasswordHash !== clipboard.password_hash) {
          return new Response(JSON.stringify({ 
            success: false, 
            error: '密码错误' 
          }), { headers: corsHeaders });
        }

        try {
          const decryptedContent = await decryptText(clipboard.content, password);
          return new Response(JSON.stringify({ 
            success: true, 
            content: decryptedContent 
          }), { headers: corsHeaders });
        } catch {
          return new Response(JSON.stringify({ 
            success: false, 
            error: '解密失败' 
          }), { headers: corsHeaders });
        }
      }

      return new Response(JSON.stringify({ 
        success: true, 
        content: clipboard.content 
      }), { headers: corsHeaders });
    }

    return new Response(JSON.stringify({ 
      success: false, 
      error: 'API not found' 
    }), { status: 404, headers: corsHeaders });

  } catch (error) {
    console.error('API error:', error);
    return new Response(JSON.stringify({ 
      success: false, 
      error: 'Internal server error' 
    }), { status: 500, headers: corsHeaders });
  }
}

// 主处理函数
async function handleRequest(request: Request): Promise<Response> {
  const url = new URL(request.url);
  const path = url.pathname;

  if (request.method === 'OPTIONS') {
    return new Response(null, {
      status: 200,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      }
    });
  }

  try {
    // 如果 OAuth2 配置不完整,显示配置页面
    if (!isConfigured()) {
      return new Response(getConfigPage(), {
        headers: { 'Content-Type': 'text/html; charset=utf-8' }
      });
    }

    // 登录页面
    if (path === '/login') {
      return new Response(getLoginPage(), {
        headers: { 'Content-Type': 'text/html; charset=utf-8' }
      });
    }

    // API 路由
    if (path.startsWith('/api/') || path.startsWith('/auth/')) {
      return await handleAPI(request, path);
    }

    // 主页面路由
    if (path === '/' || path.startsWith('/s/')) {
      const userSession = await verifyUserSession(request);
      if (!userSession) {
        return new Response(null, {
          status: 302,
          headers: { 'Location': '/login' }
        });
      }

      let shareCode = '';
      if (path.startsWith('/s/')) {
        shareCode = path.substring(3);
      } else {
        shareCode = generateShareCode();
        return new Response(null, {
          status: 302,
          headers: { 'Location': `/s/${shareCode}` }
        });
      }

      // 检查剪贴板是否存在和加密
      let clipboardExists = false;
      let isEncrypted = false;
      
      if (shareCode) {
        const result = await kv.get(['clipboards', shareCode]);
        const clipboard = result.value as any;
        
        if (clipboard) {
          clipboardExists = true;
          isEncrypted = clipboard.encrypted || false;
          
          // 检查是否过期
          if (clipboard.expires_at && new Date(clipboard.expires_at) < new Date()) {
            await kv.delete(['clipboards', shareCode]);
            const newShareCode = generateShareCode();
            return new Response(null, {
              status: 302,
              headers: { 'Location': `/s/${newShareCode}` }
            });
          }
        }
      }
      
      const html = getHomePage(shareCode, userSession, clipboardExists, isEncrypted);
      return new Response(html, {
        headers: { 'Content-Type': 'text/html; charset=utf-8' }
      });
    }

    return new Response('Not Found', { status: 404 });

  } catch (error) {
    console.error('Request error:', error);
    return new Response('Internal Server Error', { status: 500 });
  }
}

// 启动服务器
async function startServer() {
  try {
    await initKV();
    
    console.log("Server starting on port 8000...");
    console.log(`Configuration status: ${isConfigured() ? 'Complete' : 'Incomplete'}`);
    
    if (isConfigured()) {
      console.log(`OAuth2 Provider: ${OAUTH2_BASE_URL}`);
      console.log(`OAuth2 Client ID: ${OAUTH2_CLIENT_ID}`);
      console.log(`OAuth2 Redirect URI: ${OAUTH2_REDIRECT_URI}`);
      
      // 启动清理任务
      startCleanupTasks();
    } else {
      console.log("OAuth2 configuration incomplete - showing config page");
    }
    
    await serve(handleRequest, { port: 8000 });
  } catch (error) {
    console.error("Failed to start server:", error);
    throw error;
  }
}

startServer();
5 个赞

很厉害,:+1:感觉不如微信输入法自带的方便

源码好长哟,花了不少时间吧。

感谢大佬分享~ :xhj003:

为技术佬疯狂打call :xhj006:

有没有直接使用不用登陆的版本?

enclosed.cc
可以拿claw cloud部署

1 个赞

要是有阅后即焚就好了,或者换成自定义账号密码登陆限制 :xhj003:
baseurl要写 https://conn.nodeloc.cc 才行,写项目的url不行 :xhj24:

牛牛牛