使用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();