From 44095c5a90c0f6a397eb21e753e6c17e0d26e466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=B0=8F=E7=99=BD=E5=93=A6?= Date: Thu, 23 Jul 2026 02:18:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=E3=80=8C/=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | Bin 57 -> 1024 bytes functions.php | 962 +++++++++++++++++++++++++++++++ index.php | 114 ++++ install.php | 98 ++++ style.css | 1514 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 2688 insertions(+) create mode 100644 functions.php create mode 100644 index.php create mode 100644 install.php create mode 100644 style.css diff --git a/README.md b/README.md index 04ad16792040e5c112ad3d5957f7a11434aa7c95..06d7405020018ddf3cacee90fd4af10487da3d20 100644 GIT binary patch literal 1024 ScmZQz7zLvtFd70QH3R?z00031 literal 57 zcmV-90LK3#AaG%FYprepare("SELECT id FROM users WHERE username = ?"); + $stmt->execute([$username]); + if ($stmt->fetch()) { + return ['success' => false, 'message' => '用户名已被使用']; + } + + // 检查邮箱是否已存在 + $stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?"); + $stmt->execute([$email]); + if ($stmt->fetch()) { + return ['success' => false, 'message' => '邮箱已被注册']; + } + + $pdo->beginTransaction(); + try { + $hashedPassword = password_hash($password, PASSWORD_DEFAULT); + $verificationToken = bin2hex(random_bytes(32)); + $inviteCodeGen = strtoupper(substr(bin2hex(random_bytes(6)), 0, 8)); + + // 处理邀请 + $invitedBy = null; + if ($inviteCode) { + $stmt = $pdo->prepare("SELECT id FROM users WHERE invite_code = ?"); + $stmt->execute([$inviteCode]); + $inviter = $stmt->fetch(); + if ($inviter) { + $invitedBy = $inviter['id']; + } + } + + // 创建用户 + $stmt = $pdo->prepare("INSERT INTO users (username, email, password, invite_code, invited_by, verification_token) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([$username, $email, $hashedPassword, $inviteCodeGen, $invitedBy, $verificationToken]); + $userId = $pdo->lastInsertId(); + + // 如果是邀请注册,给邀请人加积分 + if ($invitedBy) { + // 增加邀请人积分 + $stmt = $pdo->prepare("UPDATE users SET credits = credits + ? WHERE id = ?"); + $stmt->execute([INVITE_CREDIT, $invitedBy]); + + // 记录邀请日志 + $stmt = $pdo->prepare("INSERT INTO invite_logs (inviter_id, invitee_id, credits_earned) VALUES (?, ?, ?)"); + $stmt->execute([$invitedBy, $userId, INVITE_CREDIT]); + } + + $pdo->commit(); + + // 发送验证邮件 + sendVerificationEmail($email, $username, $verificationToken); + + return ['success' => true, 'message' => '注册成功!请查收验证邮件']; + + } catch (Exception $e) { + $pdo->rollBack(); + return ['success' => false, 'message' => '注册失败: ' . $e->getMessage()]; + } +} + +/** + * 用户登录 + */ +function loginUser($username, $password) { + $pdo = getDB(); + + $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? OR email = ?"); + $stmt->execute([$username, $username]); + $user = $stmt->fetch(); + + if (!$user) { + return ['success' => false, 'message' => '用户不存在']; + } + + if (!$user['is_verified']) { + return ['success' => false, 'message' => '请先验证邮箱']; + } + + if (!password_verify($password, $user['password'])) { + return ['success' => false, 'message' => '密码错误']; + } + + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['is_admin'] = $user['is_admin']; + + return ['success' => true, 'message' => '登录成功']; +} + +/** + * 发送验证邮件(修复乱码问题) + */ +function sendVerificationEmail($email, $username, $token) { + $mail = new PHPMailer(true); + + try { + $mail->isSMTP(); + $mail->Host = SMTP_HOST; + $mail->SMTPAuth = true; + $mail->Username = SMTP_USERNAME; + $mail->Password = SMTP_PASSWORD; + $mail->SMTPSecure = SMTP_SECURE; + $mail->Port = SMTP_PORT; + + // 设置字符编码 - 解决乱码问题 + $mail->CharSet = 'UTF-8'; + $mail->Encoding = 'base64'; + + $mail->setFrom(SMTP_FROM_EMAIL, SMTP_FROM_NAME); + $mail->addAddress($email, $username); + $mail->isHTML(true); + + $verifyLink = SITE_URL . "/verify.php?token=" . $token; + $mail->Subject = '验证邮箱 - ' . SITE_NAME; + + // 邮件正文 - 使用完整的HTML模板 + $mail->Body = ' + + + + + + 邮箱验证 + + + +
+
+

📧 ' . SITE_NAME . '

+

验证您的邮箱地址

+
+
+

您好,' . htmlspecialchars($username) . '

+

感谢您注册 ' . SITE_NAME . '!请点击下方按钮验证您的邮箱地址:

+

+ ✅ 验证邮箱 +

+

如果按钮无法点击,请复制以下链接到浏览器打开:

+ +

+ ⏰ 此链接 24 小时 内有效。
+ 🔒 如果您没有注册 ' . SITE_NAME . ',请忽略并删除此邮件。 +

+
+ +
+ + + '; + + $mail->AltBody = "欢迎注册 " . SITE_NAME . "!\n\n" . + "您好," . $username . ":\n\n" . + "请访问以下链接验证您的邮箱地址:\n" . + $verifyLink . "\n\n" . + "此链接 24 小时内有效。\n\n" . + "如果您没有注册 " . SITE_NAME . ",请忽略并删除此邮件。"; + + $mail->send(); + return true; + + } catch (Exception $e) { + error_log("邮件发送失败: " . $e->getMessage()); + return false; + } +} + +/** + * 验证邮箱 + */ +function verifyEmail($token) { + $pdo = getDB(); + $stmt = $pdo->prepare("SELECT id, username, email FROM users WHERE verification_token = ? AND is_verified = 0"); + $stmt->execute([$token]); + $user = $stmt->fetch(); + + if (!$user) { + // 检查token是否已被使用(已验证) + $stmt = $pdo->prepare("SELECT id, username FROM users WHERE verification_token = ? AND is_verified = 1"); + $stmt->execute([$token]); + if ($stmt->fetch()) { + return ['success' => false, 'message' => '此链接已被使用,请直接登录。']; + } + return ['success' => false, 'message' => '无效的验证链接,请检查是否正确复制。']; + } + + // 更新用户状态 + $stmt = $pdo->prepare("UPDATE users SET is_verified = 1, verification_token = NULL WHERE id = ?"); + $stmt->execute([$user['id']]); + + return ['success' => true, 'message' => '邮箱验证成功!']; +} + +// ===== 积分相关函数 ===== + +/** + * 获取用户积分 + */ +function getUserCredits($userId) { + $pdo = getDB(); + $stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $result = $stmt->fetch(); + return $result ? $result['credits'] : 0; +} + +/** + * 扣减积分 + */ +function deductCredits($userId, $amount) { + $pdo = getDB(); + + // 检查积分是否足够 + $stmt = $pdo->prepare("SELECT credits FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + if (!$user || $user['credits'] < $amount) { + return ['success' => false, 'message' => '剩余的积分不足。']; + } + + $stmt = $pdo->prepare("UPDATE users SET credits = credits - ? WHERE id = ?"); + $stmt->execute([$amount, $userId]); + + return ['success' => true, 'message' => '扣减成功!']; +} + +/** + * 添加积分 + */ +function addCredits($userId, $amount, $reason = '') { + $pdo = getDB(); + $stmt = $pdo->prepare("UPDATE users SET credits = credits + ? WHERE id = ?"); + $stmt->execute([$amount, $userId]); + return ['success' => true]; +} + +// ===== 签到相关函数 ===== + +/** + * 签到 + */ +function signIn($userId) { + $pdo = getDB(); + $today = date('Y-m-d'); + + // 检查今天是否已签到 + $stmt = $pdo->prepare("SELECT id FROM sign_in_logs WHERE user_id = ? AND sign_in_date = ?"); + $stmt->execute([$userId, $today]); + if ($stmt->fetch()) { + return ['success' => false, 'message' => '今日已经签到过了。']; + } + + $pdo->beginTransaction(); + try { + // 记录签到 + $stmt = $pdo->prepare("INSERT INTO sign_in_logs (user_id, sign_in_date, credits_earned) VALUES (?, ?, ?)"); + $stmt->execute([$userId, $today, SIGN_IN_CREDIT]); + + // 增加积分 + $stmt = $pdo->prepare("UPDATE users SET credits = credits + ? WHERE id = ?"); + $stmt->execute([SIGN_IN_CREDIT, $userId]); + + $pdo->commit(); + return ['success' => true, 'message' => '签到成功!获得 ' . SIGN_IN_CREDIT . ' 积分', 'credits' => SIGN_IN_CREDIT]; + + } catch (Exception $e) { + $pdo->rollBack(); + return ['success' => false, 'message' => '签到失败: ' . $e->getMessage()]; + } +} + +/** + * 检查今天是否已签到 + */ +function hasSignedInToday($userId) { + $pdo = getDB(); + $today = date('Y-m-d'); + $stmt = $pdo->prepare("SELECT id FROM sign_in_logs WHERE user_id = ? AND sign_in_date = ?"); + $stmt->execute([$userId, $today]); + return (bool)$stmt->fetch(); +} + +/** + * 获取签到统计 + */ +function getSignInStats($userId) { + $pdo = getDB(); + $stmt = $pdo->prepare("SELECT COUNT(*) as total, SUM(credits_earned) as total_credits FROM sign_in_logs WHERE user_id = ?"); + $stmt->execute([$userId]); + return $stmt->fetch(); +} + +// ===== 域名相关函数 ===== + +/** + * 获取用户域名数量 + */ +function getUserDomainCount($userId) { + $pdo = getDB(); + $stmt = $pdo->prepare("SELECT COUNT(*) as count FROM domains WHERE user_id = ? AND status = 1"); + $stmt->execute([$userId]); + $result = $stmt->fetch(); + return $result ? $result['count'] : 0; +} + +/** + * 检查用户是否可以添加域名 + */ +function canAddDomain($userId) { + $count = getUserDomainCount($userId); + if ($count >= MAX_DOMAINS_PER_USER) { + return ['success' => false, 'message' => '您的域名数量已达到最大域名数量限制 (' . MAX_DOMAINS_PER_USER . '个)。']; + } + + $credits = getUserCredits($userId); + if ($credits < CREDIT_PER_DOMAIN) { + return ['success' => false, 'message' => '积分不足,需要 ' . CREDIT_PER_DOMAIN . ' 积分']; + } + + return ['success' => true]; +} + +/** + * 验证子域名是否合法 + */ +function validateSubdomain($subdomain) { + // 不能为空 + if (empty($subdomain)) { + return ['success' => false, 'message' => '子域名不能为空。']; + } + + // 长度至少3个字符 + if (strlen($subdomain) < 3) { + return ['success' => false, 'message' => '子域名长度至少 3 个字符。']; + } + + // 不能超过 20 个字符 + if (strlen($subdomain) > 20) { + return ['success' => false, 'message' => '子域名长度不能超过 20 个字符。']; + } + + // 只能包含小写字母、数字和连字符 + if (!preg_match('/^[a-z0-9\-]+$/', $subdomain)) { + return ['success' => false, 'message' => '子域名只能包含小写字母、数字和连字符。']; + } + + // 不能以连字符开头或结尾 + if (preg_match('/^-|-$/', $subdomain)) { + return ['success' => false, 'message' => '子域名不能以连字符开头或结尾。']; + } + + // 不能包含连续的连字符 + if (strpos($subdomain, '--') !== false) { + return ['success' => false, 'message' => '子域名不能包含连续的连字符。']; + } + + // 不能是保留关键字 + $reserved = ['www', 'mail', 'ftp', 'smtp', 'pop3', 'imap', 'admin', 'root', 'test', 'demo', 'api', 'dev', 'prod', 'stage', 'local', 'git', 'store', 'assets', 'image', 'bbs', 'help', 'forum']; + if (in_array(strtolower($subdomain), $reserved)) { + return ['success' => false, 'message' => '该子域名前缀已被禁用,请使用其他前缀。']; + } + + return ['success' => true]; +} + +/** + * 验证记录值格式 + */ +function validateRecordValue($type, $value) { + $type = strtoupper($type); + + switch ($type) { + case 'A': + if (!filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return ['success' => false, 'message' => 'A 记录需要有效的 IPv4 地址。']; + } + break; + + case 'AAAA': + if (!filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return ['success' => false, 'message' => 'AAAA 记录需要有效的 IPv6 地址。']; + } + break; + + case 'CNAME': + if (!preg_match('/^([a-z0-9\-]+\.)+[a-z]{2,}$/i', $value)) { + return ['success' => false, 'message' => 'CNAME 记录需要有效的域名。']; + } + break; + + case 'MX': + if (!preg_match('/^\d+\s+([a-z0-9\-]+\.)+[a-z]{2,}$/i', $value)) { + return ['success' => false, 'message' => 'MX 记录格式: 优先级 域名 (10 mail.example.com)。']; + } + break; + + case 'NS': + if (!preg_match('/^([a-z0-9\-]+\.)+[a-z]{2,}$/i', $value)) { + return ['success' => false, 'message' => 'NS 记录需要有效的域名。']; + } + break; + + case 'TXT': + if (strlen($value) > 255) { + return ['success' => false, 'message' => 'TXT 记录值不能超过 255 个字符。']; + } + break; + + case 'SRV': + // SRV记录格式: 优先级 权重 端口 目标域名 + $parts = preg_split('/\s+/', trim($value)); + if (count($parts) != 4) { + return ['success' => false, 'message' => 'SRV 记录格式: 优先级 权重 端口 目标域名 (10 5 5060 sip.example.com)。']; + } + + $priority = intval($parts[0]); + $weight = intval($parts[1]); + $port = intval($parts[2]); + $target = strtolower(trim($parts[3])); + + if ($priority < 0 || $priority > 65535) { + return ['success' => false, 'message' => '优先级必须是 0-65535 之间的数字。']; + } + if ($weight < 0 || $weight > 65535) { + return ['success' => false, 'message' => '权重必须是 0-65535 之间的数字。']; + } + if ($port < 0 || $port > 65535) { + return ['success' => false, 'message' => '端口必须是 0-65535 之间的数字。']; + } + if ($target !== '.' && !preg_match('/^([a-z0-9\-]+\.)+[a-z]{2,}$/', $target)) { + return ['success' => false, 'message' => '无效的目标域名,请输入有效的域名或用 . 表示空。']; + } + break; + + default: + return ['success' => false, 'message' => '不支持的记录类型。']; + } + + return ['success' => true]; +} + +/** + * 获取记录类型列表 + */ +function getRecordTypes() { + return [ + 'A' => 'IPv4 地址', + 'AAAA' => 'IPv6 地址', + 'CNAME' => '域名别名', + 'MX' => '邮件交换 (优先级 域名)', + 'NS' => '域名服务器', + 'TXT' => '文本记录', + 'SRV' => '服务定位 (优先级 权重 端口 目标)' + ]; +} + +/** + * 获取记录类型的示例值 + */ +function getRecordTypeExample($type) { + $examples = [ + 'A' => '192.168.1.1', + 'AAAA' => '2001:db8::1', + 'CNAME' => 'example.com', + 'MX' => '10 mail.example.com', + 'NS' => 'ns1.example.com', + 'TXT' => 'v=spf1 include:example.com ~all', + 'SRV' => '10 5 5060 sip.example.com' + ]; + return $examples[$type] ?? ''; +} + +/** + * 获取记录类型的格式说明 + */ +function getRecordTypeHint($type) { + $hints = [ + 'A' => 'IPv4 地址,例如: 192.168.1.1', + 'AAAA' => 'IPv6 地址,例如: 2001:db8::1', + 'CNAME' => '目标域名,例如: example.com', + 'MX' => '格式: 优先级 域名,例如: 10 mail.example.com', + 'NS' => '域名服务器,例如: ns1.example.com', + 'TXT' => '任意文本,例如: v=spf1 include:example.com ~all', + 'SRV' => '格式: 优先级 权重 端口 目标域名,例如: 10 5 5060 sip.example.com' + ]; + return $hints[$type] ?? ''; +} + +/** + * 添加域名(扣除积分) + */ +function addDomain($userId, $mainDomain, $subdomain, $type, $value) { + // 检查权限 + $canAdd = canAddDomain($userId); + if (!$canAdd['success']) { + return $canAdd; + } + + // 验证子域名 + $subdomainValidation = validateSubdomain($subdomain); + if (!$subdomainValidation['success']) { + return $subdomainValidation; + } + + // 验证记录值 + $valueValidation = validateRecordValue($type, $value); + if (!$valueValidation['success']) { + return $valueValidation; + } + + // 检查主域名是否支持 + $mainDomains = getMainDomains(); + if (!in_array($mainDomain, $mainDomains)) { + return ['success' => false, 'message' => '不支持的主域名。']; + } + + $pdo = getDB(); + + // 检查数据库中是否已存在 + $stmt = $pdo->prepare("SELECT id FROM domains WHERE main_domain = ? AND subdomain = ? AND status = 1"); + $stmt->execute([$mainDomain, $subdomain]); + if ($stmt->fetch()) { + return ['success' => false, 'message' => '该域名已存在。']; + } + + $pdo->beginTransaction(); + + try { + // 先尝试添加到 Cloudflare + $cfResult = addCloudflareRecord($mainDomain, $subdomain, $type, $value); + if (!$cfResult['success']) { + return ['success' => false, 'message' => '添加失败: ' . $cfResult['message']]; + } + + // 扣减积分 + $stmt = $pdo->prepare("UPDATE users SET credits = credits - ? WHERE id = ? AND credits >= ?"); + $stmt->execute([CREDIT_PER_DOMAIN, $userId, CREDIT_PER_DOMAIN]); + if ($stmt->rowCount() === 0) { + throw new Exception('积分不足'); + } + + // 添加域名到数据库 + $stmt = $pdo->prepare("INSERT INTO domains (user_id, main_domain, subdomain, record_type, record_value) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$userId, $mainDomain, $subdomain, $type, $value]); + + $pdo->commit(); + return ['success' => true, 'message' => '域名添加成功!消耗 ' . CREDIT_PER_DOMAIN . ' 积分']; + + } catch (Exception $e) { + $pdo->rollBack(); + // 如果数据库失败,尝试删除 Cloudflare 记录 + deleteCloudflareRecord($mainDomain, $subdomain); + return ['success' => false, 'message' => '添加失败: ' . $e->getMessage()]; + } +} + +/** + * 获取用户的域名列表 + */ +function getUserDomains($userId) { + $pdo = getDB(); + $stmt = $pdo->prepare("SELECT * FROM domains WHERE user_id = ? AND status = 1 ORDER BY created_at DESC"); + $stmt->execute([$userId]); + return $stmt->fetchAll(); +} + +/** + * 删除域名 + */ +function deleteDomain($userId, $domainId) { + $pdo = getDB(); + + // 检查域名是否属于该用户 + $stmt = $pdo->prepare("SELECT main_domain, subdomain FROM domains WHERE id = ? AND user_id = ? AND status = 1"); + $stmt->execute([$domainId, $userId]); + $domain = $stmt->fetch(); + + if (!$domain) { + return ['success' => false, 'message' => '该域名不存在。']; + } + + // 从 Cloudflare 删除 + deleteCloudflareRecord($domain['main_domain'], $domain['subdomain']); + + // 软删除 + $stmt = $pdo->prepare("UPDATE domains SET status = 0 WHERE id = ?"); + $stmt->execute([$domainId]); + + return ['success' => true, 'message' => '域名删除成功。']; +} + +// ===== Cloudflare API函数 ===== + +/** + * 获取域名配置 + */ +function getDomainConfig($mainDomain) { + global $DOMAINS_CONFIG; + return $DOMAINS_CONFIG[$mainDomain] ?? null; +} + +/** + * 添加Cloudflare记录(最终修复SRV - 使用正确的字段名) + */ +function addCloudflareRecord($mainDomain, $subdomain, $type, $value) { + $config = getDomainConfig($mainDomain); + if (!$config) { + error_log("Cloudflare 错误: 未找到域名配置 - {$mainDomain}"); + return ['success' => false, 'message' => '未找到域名配置']; + } + + $fullDomain = strtolower($subdomain) . '.' . strtolower($mainDomain); + $type = strtoupper($type); + + // 检查记录是否已存在 + $ch = curl_init('https://api.cloudflare.com/client/v4/zones/' . $config['zone_id'] . '/dns_records?name=' . urlencode($fullDomain) . '&type=' . $type); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $config['api_token'] + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $result = json_decode($response, true); + if ($result && $result['success'] && !empty($result['result'])) { + error_log("Cloudflare 记录已存在 - {$fullDomain}"); + return ['success' => false, 'message' => '该记录已存在。']; + } + } + + // 构建请求数据 - 基础字段 + $data = [ + 'type' => $type, + 'name' => $fullDomain, + 'ttl' => 300, + 'proxied' => false // 必须明确设置为 false + ]; + + // 根据记录类型处理值 + switch ($type) { + case 'A': + if (!filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return ['success' => false, 'message' => '无效的 IPv4 地址。']; + } + $data['content'] = $value; + break; + + case 'AAAA': + if (!filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return ['success' => false, 'message' => '无效的 IPv6 地址。']; + } + $data['content'] = $value; + break; + + case 'CNAME': + $value = strtolower(trim($value)); + if (!preg_match('/^([a-z0-9\-]+\.)+[a-z]{2,}$/', $value)) { + return ['success' => false, 'message' => '无效的域名格式。']; + } + $data['content'] = $value; + break; + + case 'MX': + $parts = preg_split('/\s+/', trim($value)); + if (count($parts) != 2) { + return ['success' => false, 'message' => 'MX 记录格式: 优先级 域名 (例如: 10 mail.example.com)。']; + } + if (!is_numeric($parts[0]) || intval($parts[0]) < 0 || intval($parts[0]) > 65535) { + return ['success' => false, 'message' => '优先级必须是 0-65535 之间的数字。']; + } + $domain = strtolower(trim($parts[1])); + if (!preg_match('/^([a-z0-9\-]+\.)+[a-z]{2,}$/', $domain)) { + return ['success' => false, 'message' => '无效的邮件服务器域名。']; + } + $data['priority'] = intval($parts[0]); + $data['content'] = $domain; + break; + + case 'NS': + $value = strtolower(trim($value)); + if (!preg_match('/^([a-z0-9\-]+\.)+[a-z]{2,}$/', $value)) { + return ['success' => false, 'message' => '无效的域名服务器地址。']; + } + $data['content'] = $value; + break; + + case 'TXT': + if (strlen($value) > 255) { + return ['success' => false, 'message' => 'TXT 记录值不能超过 255 字符。']; + } + $data['content'] = $value; + break; + + case 'SRV': + // ===== SRV记录特殊处理 ===== + $parts = preg_split('/\s+/', trim($value)); + if (count($parts) != 4) { + return ['success' => false, 'message' => 'SRV 记录格式: 优先级 权重 端口 目标域名 (例如: 10 5 5060 sip.example.com)']; + } + + $priority = intval($parts[0]); + $weight = intval($parts[1]); + $port = intval($parts[2]); + $target = strtolower(trim($parts[3])); + + // 验证数值范围 + if ($priority < 0 || $priority > 65535) { + return ['success' => false, 'message' => '优先级必须是 0-65535 之间的数字。']; + } + if ($weight < 0 || $weight > 65535) { + return ['success' => false, 'message' => '权重必须是 0-65535 之间的数字。']; + } + if ($port < 0 || $port > 65535) { + return ['success' => false, 'message' => '端口必须是 0-65535 之间的数字。']; + } + + // 验证目标域名 + if ($target !== '.' && !preg_match('/^([a-z0-9\-]+\.)+[a-z]{2,}$/', $target)) { + return ['success' => false, 'message' => '无效的目标域名,请输入有效的域名或用 . 表示空。']; + } + + // ===== 重要:SRV 记录使用不同的字段名 ===== + // Cloudflare SRV API 要求使用这些字段名: + // - priority, weight, port, target (不是 content!) + $data['priority'] = (int)$priority; + $data['weight'] = (int)$weight; + $data['port'] = (int)$port; + $data['target'] = (string)$target; // 关键:使用 target 而不是 content + $data['proxied'] = false; + + error_log("SRV 数据: priority={$priority}, weight={$weight}, port={$port}, target={$target}"); + break; + + default: + return ['success' => false, 'message' => '不支持的记录类型']; + } + + // 记录请求数据用于调试 + error_log("Cloudflare 请求数据: " . json_encode($data)); + + $ch = curl_init('https://api.cloudflare.com/client/v4/zones/' . $config['zone_id'] . '/dns_records'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $config['api_token'] + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + // 记录响应 + error_log("Cloudflare 响应: HTTP {$httpCode}"); + error_log("Cloudflare 响应内容: " . $response); + + if ($curlError) { + error_log("Cloudflare CURL 错误: " . $curlError); + return ['success' => false, 'message' => 'CURL 错误: ' . $curlError]; + } + + if ($httpCode === 200) { + $result = json_decode($response, true); + if ($result && $result['success']) { + error_log("Cloudflare 添加成功: {$fullDomain}"); + return ['success' => true, 'message' => '添加成功']; + } else { + $errorMsg = '未知错误'; + if (isset($result['errors']) && !empty($result['errors'])) { + $errorMsg = $result['errors'][0]['message'] ?? 'API错误'; + if (isset($result['errors'][0]['error_chain'])) { + foreach ($result['errors'][0]['error_chain'] as $chain) { + if (isset($chain['message'])) { + $errorMsg .= ' - ' . $chain['message']; + } + } + } + } + error_log("Cloudflare API 错误: " . $errorMsg); + return ['success' => false, 'message' => 'Cloudflare 错误: ' . $errorMsg]; + } + } + + // 解析错误响应 + $result = json_decode($response, true); + if ($result && isset($result['errors'])) { + $errorMsg = ''; + foreach ($result['errors'] as $error) { + $errorMsg .= $error['message'] . ' '; + if (isset($error['error_chain'])) { + foreach ($error['error_chain'] as $chain) { + if (isset($chain['message'])) { + $errorMsg .= '- ' . $chain['message'] . ' '; + } + } + } + } + return ['success' => false, 'message' => '请求错误: ' . trim($errorMsg)]; + } + + return ['success' => false, 'message' => "HTTP错误: {$httpCode} - " . substr($response, 0, 200)]; +} + +/** + * 删除Cloudflare记录(支持SRV) + */ +function deleteCloudflareRecord($mainDomain, $subdomain) { + $config = getDomainConfig($mainDomain); + if (!$config) return false; + + $fullDomain = $subdomain . '.' . $mainDomain; + $zoneId = $config['zone_id']; + $token = $config['api_token']; + + // 查询记录ID - 不限定类型,因为SRV记录可能用target字段 + $ch = curl_init('https://api.cloudflare.com/client/v4/zones/' . $zoneId . '/dns_records?name=' . urlencode($fullDomain)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $token + ]); + + $response = curl_exec($ch); + $result = json_decode($response, true); + curl_close($ch); + + if (isset($result['result']) && !empty($result['result'])) { + $recordId = $result['result'][0]['id']; + + $ch = curl_init('https://api.cloudflare.com/client/v4/zones/' . $zoneId . '/dns_records/' . $recordId); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $token + ]); + + curl_exec($ch); + curl_close($ch); + return true; + } + + return false; +} + +// ===== 其他辅助函数 ===== + +/** + * 检查是否已登录 + */ +function isLoggedIn() { + return isset($_SESSION['user_id']); +} + +/** + * 检查是否是管理员 + */ +function isAdmin() { + return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] == 1; +} + +/** + * 获取当前用户ID + */ +function getCurrentUserId() { + return $_SESSION['user_id'] ?? null; +} + +/** + * 获取用户信息 + */ +function getUserInfo($userId) { + $pdo = getDB(); + $stmt = $pdo->prepare("SELECT id, username, email, credits, invite_code, is_admin, created_at FROM users WHERE id = ?"); + $stmt->execute([$userId]); + return $stmt->fetch(); +} + +/** + * 获取邀请统计 + */ +function getInviteStats($userId) { + $pdo = getDB(); + $stmt = $pdo->prepare("SELECT COUNT(*) as count, SUM(credits_earned) as total_credits FROM invite_logs WHERE inviter_id = ?"); + $stmt->execute([$userId]); + return $stmt->fetch(); +} + +/** + * 获取主题 + */ +function getTheme() { + $pdo = getDB(); + try { + $stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = 'theme'"); + $stmt->execute(); + $result = $stmt->fetch(); + return $result ? $result['setting_value'] : 'dark'; + } catch (PDOException $e) { + return 'dark'; + } +} + +/** + * 切换主题 + */ +function toggleTheme() { + $pdo = getDB(); + $current = getTheme(); + $new = $current === 'dark' ? 'light' : 'dark'; + $stmt = $pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'theme'"); + $stmt->execute([$new]); + return $new; +} + +/** + * 获取所有主域名列表 + */ +function getMainDomains() { + global $DOMAINS_CONFIG; + return array_keys($DOMAINS_CONFIG); +} +?> \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..ae4f9e1 --- /dev/null +++ b/index.php @@ -0,0 +1,114 @@ +query("SELECT COUNT(*) as count FROM domains WHERE status = 1")->fetch()['count']; +$totalUsers = $pdo->query("SELECT COUNT(*) as count FROM users WHERE is_verified = 1")->fetch()['count']; +$stmt = $pdo->prepare("SELECT d.*, u.username FROM domains d LEFT JOIN users u ON d.user_id = u.id WHERE d.status = 1 ORDER BY d.created_at DESC LIMIT 10"); +$stmt->execute(); +$recentDomains = $stmt->fetchAll(); + +$currentTheme = getTheme(); +$mainDomains = getMainDomains(); +?> + + + + + + <?php echo SITE_NAME; ?> - 用心制作的免费二级域名分发网站 + + + + +
+
+
+

+
+
+ + +
+ + + + + + +
+
+
+
+

免费二级域名分发

+

+ 轻松获取免费的二级域名,支持 A、AAAA、CNAME、MX、NS、TXT 记录 +
积分制管理,每日签到免费领取积分 +

+ +
+ + +
+
+
+
已有域名
+
+
+
+
注册用户
+
+
+
+
支持域名
+
+
+ + +
+
+ +

积分制管理

+

注册送 10 积分,每日签到得 2 积分,邀请好友得 10 积分

+
+
+ +

完整 DNS 记录

+

支持 A、AAAA、CNAME、MX、NS、TXT 全部记录类型

+
+
+ +

Cloudflare 驱动

+

域名自动托管到 Cloudflare,全球节点加速,稳定可靠

+
+
+ +

邀请奖励

+

邀请好友注册,双方都获得积分奖励,共同成长

+
+
+
+

+ © + · 免费二级域名分发系统 + · 由 驱动 +

+

+ 所有数据安全加密 +

+
+
+ + \ No newline at end of file diff --git a/install.php b/install.php new file mode 100644 index 0000000..83c81f6 --- /dev/null +++ b/install.php @@ -0,0 +1,98 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // 创建数据库 + $pdo->exec("CREATE DATABASE IF NOT EXISTS " . DB_NAME); + $pdo->exec("USE " . DB_NAME); + + // 用户表 + $pdo->exec("CREATE TABLE IF NOT EXISTS users ( + id INT PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + credits INT DEFAULT 10, + invite_code VARCHAR(20) UNIQUE NOT NULL, + invited_by INT DEFAULT NULL, + is_admin TINYINT DEFAULT 0, + is_verified TINYINT DEFAULT 0, + verification_token VARCHAR(64), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_sign_in DATE, + INDEX idx_invite_code (invite_code), + INDEX idx_email (email), + FOREIGN KEY (invited_by) REFERENCES users(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + + // 签到记录表 + $pdo->exec("CREATE TABLE IF NOT EXISTS sign_in_logs ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL, + sign_in_date DATE NOT NULL, + credits_earned INT DEFAULT 2, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY unique_signin (user_id, sign_in_date), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + + // 域名表 + $pdo->exec("CREATE TABLE IF NOT EXISTS domains ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL, + main_domain VARCHAR(100) NOT NULL, + subdomain VARCHAR(100) NOT NULL, + record_type VARCHAR(10) NOT NULL, + record_value VARCHAR(255) NOT NULL, + status TINYINT DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_domain (main_domain, subdomain), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_user (user_id), + INDEX idx_status (status) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + + // 邀请记录表 + $pdo->exec("CREATE TABLE IF NOT EXISTS invite_logs ( + id INT PRIMARY KEY AUTO_INCREMENT, + inviter_id INT NOT NULL, + invitee_id INT NOT NULL, + credits_earned INT DEFAULT 10, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (inviter_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (invitee_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE KEY unique_invitee (invitee_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + + // 系统设置表 + $pdo->exec("CREATE TABLE IF NOT EXISTS settings ( + setting_key VARCHAR(50) PRIMARY KEY, + setting_value TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + + // 插入默认设置 + $pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES + ('theme', 'dark'), + ('credit_per_domain', '5'), + ('max_domains_per_user', '10'), + ('sign_in_credit', '2'), + ('invite_credit', '10'), + ('site_name', '域名分发系统') + "); + + // 创建管理员账户(提示用户输入) + echo "

数据库安装成功!

"; + echo "

您已安装 Parlz Domains Beta 0.1。

" + echo "

请访问 注册页面 来创建管理员账户。

"; + echo "

进入首页

"; + +} catch (PDOException $e) { + die("安装失败: " . $e->getMessage()); +} +?> \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..479b776 --- /dev/null +++ b/style.css @@ -0,0 +1,1514 @@ +@import url("https://cdn.jsdmirror.com/gh/willow-god/Sharding-fonts/Yozai-Medium/result.min.css"); + +:root { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-card: #1c2333; + --bg-input: #0d1117; + --bg-hover: #1c2333; + --text-primary: #e6edf3; + --text-secondary: #8b949e; + --text-muted: #484f58; + --border-color: #30363d; + --shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + --shadow-hover: 0 12px 48px rgba(0, 0, 0, 0.6); + --accent-blue: #58a6ff; + --accent-blue-dark: #1f6feb; + --accent-green: #3fb950; + --accent-red: #f85149; + --accent-yellow: #d29922; + --accent-purple: #bc8cff; + --accent-orange: #f0883e; + --radius: 12px; + --radius-sm: 6px; + --transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +[data-theme="light"] { + --bg-primary: #f6f8fa; + --bg-secondary: #ffffff; + --bg-card: #ffffff; + --bg-input: #f6f8fa; + --bg-hover: #f3f4f6; + --text-primary: #24292f; + --text-secondary: #57606a; + --text-muted: #8b949e; + --border-color: #d0d7de; + --shadow: 0 8px 32px rgba(0, 0, 0, 0.08); + --shadow-hover: 0 12px 48px rgba(0, 0, 0, 0.12); + --accent-blue: #0969da; + --accent-blue-dark: #0550ae; + --accent-green: #1a7f37; + --accent-red: #cf222e; + --accent-yellow: #9a6700; + --accent-purple: #8250df; + --accent-orange: #bd5612 +} + +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box +} + +* { + font-family: "Yozai Medium" +} + +html { + scroll-behavior: smooth +} + +body { + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; + transition: background var(--transition), color var(--transition); + min-height: 100vh; + padding: 20px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +::-webkit-scrollbar { + width: 8px; + height: 8px +} + +::-webkit-scrollbar-track { + background: var(--bg-primary) +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; + transition: background var(--transition) +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted) +} + +.container { + max-width: 1100px; + margin: 0 auto; + padding: 0 10px +} + +header { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 18px 28px; + margin-bottom: 28px; + box-shadow: var(--shadow); + transition: all var(--transition) +} + +header:hover { + box-shadow: var(--shadow-hover) +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 15px +} + +header h1 { + font-size: 24px; + font-weight: 700; + display: flex; + align-items: center; + gap: 12px; + letter-spacing: -0.5px +} + +header h1 i { + color: var(--accent-blue); + font-size: 26px +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap +} + +.domain-badge { + background: var(--bg-primary); + padding: 6px 16px; + border-radius: 20px; + font-size: 13px; + color: var(--text-secondary); + border: 1px solid var(--border-color); + display: flex; + align-items: center; + gap: 6px; + transition: all var(--transition) +} + +.domain-badge:hover { + border-color: var(--accent-blue); + color: var(--text-primary) +} + +.theme-toggle { + background: var(--bg-primary); + border: 1px solid var(--border-color); + color: var(--text-secondary); + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + font-size: 18px; + transition: all var(--transition); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0 +} + +.theme-toggle:hover { + background: var(--accent-blue); + color: #fff; + border-color: var(--accent-blue); + transform: rotate(20deg) scale(1.05) +} + +.theme-toggle:active { + transform: rotate(40deg) scale(0.95) +} + +.btn { + padding: 10px 24px; + border: none; + border-radius: var(--radius-sm); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; + font-family: var(--font-family); + letter-spacing: 0.3px +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important +} + +.btn-primary { + background: var(--accent-blue); + color: #fff +} + +.btn-primary:hover:not(:disabled) { + opacity: 0.85; + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(88, 166, 255, 0.35) +} + +.btn-primary:active:not(:disabled) { + transform: translateY(0) +} + +.btn-success { + background: var(--accent-green); + color: #fff +} + +.btn-success:hover:not(:disabled) { + opacity: 0.85; + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(63, 185, 80, 0.35) +} + +.btn-danger { + background: var(--accent-red); + color: #fff +} + +.btn-danger:hover:not(:disabled) { + opacity: 0.85; + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(248, 81, 73, 0.35) +} + +.btn-warning { + background: var(--accent-yellow); + color: #fff +} + +.btn-warning:hover:not(:disabled) { + opacity: 0.85; + transform: translateY(-2px) +} + +.btn-outline { + background: transparent; + border: 2px solid var(--border-color); + color: var(--text-primary) +} + +.btn-outline:hover:not(:disabled) { + border-color: var(--accent-blue); + color: var(--accent-blue); + transform: translateY(-2px) +} + +.btn-sm { + padding: 5px 14px; + font-size: 12px; + border-radius: var(--radius-sm) +} + +.btn-large { + padding: 14px 36px; + font-size: 16px; + border-radius: 10px +} + +.card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + margin-bottom: 25px; + box-shadow: var(--shadow); + overflow: hidden; + transition: all var(--transition) +} + +.card:hover { + box-shadow: var(--shadow-hover) +} + +.card-header { + padding: 18px 25px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; + background: var(--bg-primary) +} + +.card-header h2 { + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px +} + +.card-header h2 i { + color: var(--accent-blue) +} + +.card-body { + padding: 25px +} + +.badge { + background: var(--accent-blue); + color: #fff; + padding: 2px 14px; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 4px +} + +.badge-success { + background: var(--accent-green) +} + +.badge-danger { + background: var(--accent-red) +} + +.badge-warning { + background: var(--accent-yellow) +} + +.add-form { + display: flex; + flex-direction: column; + gap: 18px +} + +.form-row { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 18px +} + +@media (max-width:600px) { + .form-row { + grid-template-columns: 1fr + } +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px +} + +.form-group label { + font-size: 14px; + font-weight: 500; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 8px +} + +.form-group label i { + color: var(--accent-blue); + width: 18px; + font-size: 14px +} + +.form-group input, +.form-group select, +.form-group textarea { + padding: 10px 14px; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + background: var(--bg-input); + color: var(--text-primary); + font-size: 14px; + transition: all var(--transition); + outline: none; + font-family: var(--font-family); + width: 100% +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15) +} + +.form-group input:disabled, +.form-group select:disabled { + opacity: 0.6; + cursor: not-allowed +} + +.form-group input::placeholder { + color: var(--text-muted) +} + +.input-group { + display: flex; + align-items: center +} + +.input-group input { + flex: 1; + border-radius: var(--radius-sm) 0 0 var(--radius-sm) +} + +.input-group input:focus { + border-radius: var(--radius-sm) 0 0 var(--radius-sm) +} + +.input-suffix { + padding: 10px 14px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-left: none; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + color: var(--text-secondary); + font-size: 13px; + white-space: nowrap; + font-weight: 500 +} + +.form-hint { + font-size: 12px; + color: var(--text-muted); + margin-top: 4px; + padding: 4px 10px; + background: var(--bg-primary); + border-radius: 4px; + border-left: 3px solid var(--accent-yellow) +} + +.form-hint i { + color: var(--accent-yellow); + margin-right: 4px +} + +.record-type-help { + background: var(--bg-primary); + border-radius: var(--radius-sm); + padding: 10px 14px; + font-size: 13px; + color: var(--text-secondary); + border: 1px solid var(--border-color); + margin-top: 6px; + transition: all var(--transition) +} + +.record-type-help i { + color: var(--accent-yellow); + margin-right: 6px +} + +.record-type-help code { + background: var(--bg-secondary); + padding: 2px 8px; + border-radius: 3px; + font-size: 12px; + font-family: 'SF Mono', 'Fira Code', monospace; + color: var(--text-primary); + border: 1px solid var(--border-color) +} + +.form-group input:invalid { + border-color: var(--accent-red) +} + +.form-group input:invalid:focus { + border-color: var(--accent-red); + box-shadow: 0 0 0 3px rgba(248, 81, 73, 0.15) +} + +.form-group input:valid { + border-color: var(--accent-green) +} + +.table-responsive { + overflow-x: auto; + margin: -5px; + padding: 5px +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 14px +} + +thead { + background: var(--bg-primary); + border-bottom: 2px solid var(--border-color) +} + +th { + text-align: left; + padding: 12px 16px; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + white-space: nowrap +} + +th i { + margin-right: 6px; + color: var(--accent-blue) +} + +td { + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + vertical-align: middle +} + +tbody tr { + transition: background var(--transition) +} + +tbody tr:hover { + background: var(--bg-hover) +} + +tbody tr:last-child td { + border-bottom: none +} + +.domain-name { + font-weight: 600 +} + +.domain-name small { + color: var(--text-muted); + font-weight: 400 +} + +.main-domain { + font-weight: 600; + color: var(--accent-blue); + background: var(--bg-primary); + padding: 2px 12px; + border-radius: 4px; + font-size: 13px; + border: 1px solid var(--border-color) +} + +.record-type { + display: inline-block; + padding: 2px 12px; + border-radius: 4px; + background: var(--bg-primary); + font-weight: 700; + font-size: 12px; + color: var(--accent-yellow); + border: 1px solid var(--border-color); + font-family: 'SF Mono', 'Fira Code', monospace +} + +.record-type.a { + color: var(--accent-blue) +} + +.record-type.aaaa { + color: var(--accent-purple) +} + +.record-type.cname { + color: var(--accent-green) +} + +.record-type.mx { + color: var(--accent-orange) +} + +.record-type.ns { + color: var(--accent-red) +} + +.record-type.txt { + color: var(--accent-yellow) +} + +.domain-value { + color: var(--text-secondary); + font-size: 13px; + word-break: break-all; + max-width: 300px; + font-family: 'SF Mono', 'Fira Code', monospace; + background: var(--bg-primary); + padding: 2px 10px; + border-radius: 4px; + border: 1px solid var(--border-color); + display: inline-block +} + +.domain-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + flex-wrap: wrap; + gap: 10px; + transition: background var(--transition) +} + +.domain-item:hover { + background: var(--bg-hover) +} + +.domain-item:last-child { + border-bottom: none +} + +.domain-info { + display: flex; + align-items: center; + gap: 15px; + flex-wrap: wrap +} + +.domain-full { + font-weight: 600 +} + +.domain-full small { + color: var(--text-muted); + font-weight: 400 +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 15px; + margin-bottom: 25px +} + +.stat-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 18px 20px; + text-align: center; + transition: all var(--transition); + box-shadow: var(--shadow) +} + +.stat-card:hover { + border-color: var(--accent-blue); + transform: translateY(-3px); + box-shadow: var(--shadow-hover) +} + +.stat-card .number { + font-size: 28px; + font-weight: 700; + color: var(--accent-blue); + line-height: 1.2 +} + +.stat-card .label { + font-size: 13px; + color: var(--text-secondary); + margin-top: 4px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px +} + +.stat-card.credits .number { + color: var(--accent-yellow) +} + +.stat-card.domains .number { + color: var(--accent-green) +} + +.stat-card.signin .number { + color: var(--accent-blue) +} + +.alert { + padding: 14px 20px; + border-radius: var(--radius); + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 12px; + border: 1px solid transparent; + animation: slideDown 0.4s ease +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px) + } + + to { + opacity: 1; + transform: translateY(0) + } +} + +.alert-success { + background: rgba(63, 185, 80, 0.12); + border-color: var(--accent-green); + color: var(--accent-green) +} + +.alert-error { + background: rgba(248, 81, 73, 0.12); + border-color: var(--accent-red); + color: var(--accent-red) +} + +.alert-warning { + background: rgba(210, 153, 34, 0.12); + border-color: var(--accent-yellow); + color: var(--accent-yellow) +} + +.alert-info { + background: rgba(88, 166, 255, 0.12); + border-color: var(--accent-blue); + color: var(--accent-blue) +} + +.alert i { + font-size: 18px; + flex-shrink: 0 +} + +.empty-state { + text-align: center; + padding: 50px 20px; + color: var(--text-secondary) +} + +.empty-state i { + font-size: 48px; + margin-bottom: 15px; + opacity: 0.3; + display: block +} + +.empty-state p { + font-size: 16px; + margin-bottom: 6px +} + +.empty-state .sub-text { + font-size: 13px; + color: var(--text-muted) +} + +.invite-box { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 15px 20px; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 20px; + transition: all var(--transition) +} + +.invite-box:hover { + border-color: var(--accent-yellow) +} + +.invite-code { + font-family: 'SF Mono', 'Fira Code', monospace; + font-size: 18px; + font-weight: 700; + color: var(--accent-blue); + background: var(--bg-secondary); + padding: 6px 16px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-color); + letter-spacing: 2px +} + +.copy-btn { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 6px 16px; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition); + font-size: 13px; + font-weight: 500 +} + +.copy-btn:hover { + background: var(--accent-blue); + color: #fff; + border-color: var(--accent-blue); + transform: scale(1.02) +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 20px; + margin: 40px 0 +} + +.feature-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 24px 20px; + text-align: center; + transition: all var(--transition); + box-shadow: var(--shadow) +} + +.feature-card:hover { + transform: translateY(-4px); + border-color: var(--accent-blue); + box-shadow: var(--shadow-hover) +} + +.feature-card i { + font-size: 32px; + color: var(--accent-blue); + margin-bottom: 12px; + display: block +} + +.feature-card h3 { + font-size: 16px; + margin-bottom: 6px; + font-weight: 600 +} + +.feature-card p { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.5 +} + +.hero { + text-align: center; + padding: 60px 20px 40px +} + +.hero h1 { + font-size: 42px; + font-weight: 800; + margin-bottom: 16px; + letter-spacing: -1px +} + +.hero h1 i { + color: var(--accent-blue) +} + +.hero p { + font-size: 18px; + color: var(--text-secondary); + max-width: 600px; + margin: 0 auto 30px; + line-height: 1.7 +} + +.hero .btn-group { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap +} + +.stats-bar { + display: flex; + justify-content: center; + gap: 40px; + flex-wrap: wrap; + padding: 20px 30px; + background: var(--bg-secondary); + border-radius: var(--radius); + border: 1px solid var(--border-color); + margin: 30px 0; + box-shadow: var(--shadow) +} + +.stats-bar .stat-item { + text-align: center +} + +.stats-bar .stat-number { + font-size: 28px; + font-weight: 700; + color: var(--accent-blue); + line-height: 1.2 +} + +.stats-bar .stat-label { + font-size: 13px; + color: var(--text-secondary) +} + +.recent-list { + margin-top: 0 +} + +.recent-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + border-bottom: 1px solid var(--border-color); + font-size: 14px; + flex-wrap: wrap; + gap: 8px; + transition: background var(--transition) +} + +.recent-item:hover { + background: var(--bg-hover) +} + +.recent-item:last-child { + border-bottom: none +} + +.recent-domain { + font-weight: 600 +} + +.recent-domain small { + color: var(--text-muted); + font-weight: 400 +} + +.recent-user { + color: var(--text-secondary); + font-size: 12px; + display: flex; + align-items: center; + gap: 4px +} + +.recent-type { + background: var(--bg-primary); + padding: 1px 10px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; + color: var(--accent-yellow); + border: 1px solid var(--border-color) +} + +.auth-container { + max-width: 440px; + margin: 60px auto 40px +} + +.auth-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 40px; + box-shadow: var(--shadow) +} + +.auth-card h2 { + text-align: center; + margin-bottom: 30px; + font-weight: 700; + font-size: 24px +} + +.auth-card h2 i { + color: var(--accent-blue) +} + +.auth-switch { + text-align: center; + margin-top: 20px; + color: var(--text-secondary); + font-size: 14px +} + +.auth-switch a { + color: var(--accent-blue); + text-decoration: none; + font-weight: 500 +} + +.auth-switch a:hover { + text-decoration: underline +} + +.invite-hint { + font-size: 12px; + color: var(--text-muted); + margin-top: 4px +} + +.user-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + flex-wrap: wrap; + gap: 10px; + transition: background var(--transition) +} + +.user-row:hover { + background: var(--bg-hover) +} + +.user-row:last-child { + border-bottom: none +} + +.user-info { + display: flex; + align-items: center; + gap: 15px; + flex-wrap: wrap +} + +.user-badge { + font-size: 12px; + padding: 2px 12px; + border-radius: 4px; + font-weight: 500 +} + +.user-badge.admin { + background: var(--accent-blue); + color: #fff +} + +.user-badge.user { + background: var(--bg-primary); + color: var(--text-secondary); + border: 1px solid var(--border-color) +} + +.user-badge.verified { + background: var(--accent-green); + color: #fff +} + +.user-badge.unverified { + background: var(--accent-red); + color: #fff +} + +.admin-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center +} + +.admin-actions input[type="number"] { + width: 60px; + padding: 4px 6px; + border-radius: 4px; + border: 1px solid var(--border-color); + background: var(--bg-input); + color: var(--text-primary); + font-size: 13px +} + +.admin-actions select { + padding: 4px 6px; + border-radius: 4px; + border: 1px solid var(--border-color); + background: var(--bg-input); + color: var(--text-primary); + font-size: 13px +} + +.welcome-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 15px; + margin-bottom: 25px +} + +.welcome-header h1 { + font-size: 24px; + font-weight: 700 +} + +.user-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center +} + +footer { + text-align: center; + padding: 30px 0 20px; + color: var(--text-muted); + font-size: 13px; + border-top: 1px solid var(--border-color); + margin-top: 30px +} + +footer a { + color: var(--accent-blue); + text-decoration: none +} + +footer a:hover { + text-decoration: underline +} + +footer .fa-heart { + color: var(--accent-red) +} + +@keyframes spin { + from { + transform: rotate(0deg) + } + + to { + transform: rotate(360deg) + } +} + +.fa-spinner { + animation: spin 1s linear infinite +} + +.text-center { + text-align: center +} + +.text-muted { + color: var(--text-muted) +} + +.text-secondary { + color: var(--text-secondary) +} + +.mt-10 { + margin-top: 10px +} + +.mb-10 { + margin-bottom: 10px +} + +.mt-20 { + margin-top: 20px +} + +.mb-20 { + margin-bottom: 20px +} + +.flex { + display: flex +} + +.flex-center { + display: flex; + align-items: center; + justify-content: center +} + +.gap-10 { + gap: 10px +} + +.gap-20 { + gap: 20px +} + +.w-full { + width: 100% +} + +@media (max-width:768px) { + body { + padding: 12px + } + + header { + padding: 14px 18px + } + + header h1 { + font-size: 20px + } + + .header-content { + flex-direction: column; + align-items: stretch; + gap: 12px + } + + .header-actions { + justify-content: flex-start; + flex-wrap: wrap + } + + .card-header { + flex-direction: column; + align-items: stretch; + gap: 8px + } + + .card-body { + padding: 16px + } + + .stats-grid { + grid-template-columns: 1fr 1fr; + gap: 10px + } + + .stat-card { + padding: 14px 12px + } + + .stat-card .number { + font-size: 22px + } + + .hero { + padding: 30px 10px 20px + } + + .hero h1 { + font-size: 28px + } + + .hero p { + font-size: 15px; + padding: 0 10px + } + + .hero .btn-group { + flex-direction: column; + align-items: center; + width: 100% + } + + .hero .btn-group .btn { + width: 100%; + justify-content: center + } + + .stats-bar { + gap: 20px; + padding: 16px 20px; + flex-wrap: wrap + } + + .stats-bar .stat-number { + font-size: 22px + } + + .features-grid { + grid-template-columns: 1fr 1fr; + gap: 12px; + margin: 20px 0 + } + + .feature-card { + padding: 16px 14px + } + + .feature-card i { + font-size: 24px + } + + .feature-card h3 { + font-size: 14px + } + + .feature-card p { + font-size: 12px + } + + .invite-box { + flex-direction: column; + align-items: stretch; + text-align: center + } + + .invite-code { + font-size: 16px; + text-align: center + } + + .domain-item { + flex-direction: column; + align-items: stretch; + gap: 8px; + padding: 10px 12px + } + + .domain-info { + flex-direction: column; + align-items: flex-start; + gap: 6px + } + + .domain-value { + max-width: 100%; + word-break: break-all; + font-size: 12px + } + + .domain-full { + font-size: 14px + } + + .user-row { + flex-direction: column; + align-items: stretch; + gap: 10px + } + + .user-info { + flex-direction: column; + align-items: flex-start; + gap: 6px; + font-size: 13px + } + + .admin-actions { + flex-wrap: wrap; + gap: 6px + } + + .admin-actions form { + flex-wrap: wrap + } + + .auth-container { + margin: 30px auto; + padding: 0 10px + } + + .auth-card { + padding: 24px 20px + } + + .recent-item { + flex-direction: column; + align-items: flex-start; + gap: 4px; + padding: 8px 12px; + font-size: 13px + } + + .recent-user { + font-size: 11px + } + + .record-type-help { + font-size: 12px; + padding: 8px 10px + } + + .table-responsive { + margin: 0 -10px; + padding: 0 5px + } + + table { + font-size: 12px + } + + th, + td { + padding: 8px 10px + } + + .btn-large { + padding: 12px 24px; + font-size: 14px + } + + .btn { + font-size: 13px; + padding: 8px 18px + } + + .btn-sm { + font-size: 11px; + padding: 4px 10px + } +} + +@media (max-width:480px) { + .stats-grid { + grid-template-columns: 1fr 1fr; + gap: 8px + } + + .stat-card { + padding: 10px 8px + } + + .stat-card .number { + font-size: 18px + } + + .stat-card .label { + font-size: 11px + } + + .features-grid { + grid-template-columns: 1fr 1fr; + gap: 8px + } + + .feature-card { + padding: 12px 10px + } + + .feature-card i { + font-size: 20px + } + + .feature-card h3 { + font-size: 12px + } + + .feature-card p { + font-size: 11px + } + + .hero h1 { + font-size: 22px + } + + .hero p { + font-size: 13px + } + + header h1 { + font-size: 17px + } + + .domain-badge { + font-size: 11px; + padding: 4px 10px + } + + .card-header h2 { + font-size: 15px + } + + .badge { + font-size: 11px; + padding: 1px 10px + } + + .form-group label { + font-size: 12px + } + + .form-group input, + .form-group select { + font-size: 13px; + padding: 8px 12px + } + + .invite-code { + font-size: 14px; + padding: 4px 12px + } + + .stats-bar .stat-number { + font-size: 18px + } + + .stats-bar .stat-label { + font-size: 11px + } +} \ No newline at end of file