功能:完善用户与项目管理框架

This commit is contained in:
Coldsmile_7
2026-06-13 00:50:11 +08:00
parent db6d6de74a
commit 49c86bc14a
25 changed files with 3571 additions and 192 deletions
+149 -75
View File
@@ -1,7 +1,10 @@
import { randomBytes } from "node:crypto";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { spawn } from "node:child_process";
import { request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { db, getSetting } from "./db.mjs";
import { json, readBody, redirect } from "./http.mjs";
import { getRequestSession } from "./auth.mjs";
@@ -37,7 +40,7 @@ function requestTextWithPowerShell(targetUrl, init = {}) {
).toString("base64");
const script = `
$ErrorActionPreference = 'Stop'
$payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($args[0])) | ConvertFrom-Json
$payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:VITEPRESS_CMS_PS_PAYLOAD)) | ConvertFrom-Json
$headers = @{}
if ($payload.headers) {
foreach ($property in $payload.headers.PSObject.Properties) {
@@ -81,7 +84,11 @@ try {
$result | ConvertTo-Json -Compress -Depth 5
`;
const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand, payload], {
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand], {
env: {
...process.env,
VITEPRESS_CMS_PS_PAYLOAD: payload,
},
windowsHide: true,
});
const stdout = [];
@@ -281,6 +288,11 @@ function publicCmsUser(user) {
name: user.name,
avatarUrl: user.avatar_url,
role: user.role,
status: user.status || "active",
emailVerified: Boolean(user.email_verified),
createdAt: user.created_at,
updatedAt: user.updated_at,
lastLoginAt: user.last_login_at,
};
}
@@ -392,8 +404,14 @@ async function handleCallback(request, response, url) {
const user = userResponse.payload ?? {};
const { session: currentSession } = getRequestSession(request);
const cmsUser = findOrCreateGithubUser(user, tokenPayload.access_token, currentSession);
if ((cmsUser.status || "active") !== "active") {
throw new Error("This account is disabled");
}
db.prepare("UPDATE users SET last_login_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(cmsUser.id);
const nextCmsUser = db.prepare("SELECT * FROM users WHERE id = ?").get(cmsUser.id);
const sessionId = createSession({
user: publicCmsUser(cmsUser),
user: publicCmsUser(nextCmsUser),
github: {
accessToken: tokenPayload.access_token,
login: user.login,
@@ -489,6 +507,10 @@ function normalizeSiteRoot(siteRoot = "") {
return siteRoot.trim().replace(/^\/+|\/+$/g, "");
}
function getVitePressSiteRoot(siteRoot = "") {
return normalizeSiteRoot(siteRoot) || "docs";
}
function toRepoPath(siteRoot, path) {
const normalizedRoot = normalizeSiteRoot(siteRoot);
return normalizedRoot ? `${normalizedRoot}/${path}` : path;
@@ -508,10 +530,11 @@ function encodeBase64Content(content) {
}
function packageJsonTemplate(siteRoot) {
const root = normalizeSiteRoot(siteRoot) || ".";
const root = getVitePressSiteRoot(siteRoot);
return `${JSON.stringify(
{
type: "module",
scripts: {
"docs:dev": `vitepress dev ${root}`,
"docs:build": `vitepress build ${root}`,
@@ -527,43 +550,100 @@ function packageJsonTemplate(siteRoot) {
`;
}
function siteConfigTemplate() {
return `import { defineConfig } from "vitepress";
function mergePackageJsonContent(existingSource, siteRoot) {
const root = getVitePressSiteRoot(siteRoot);
let packageJson;
export default defineConfig({
title: "VitePress Site",
description: "Created by VitePress-CMS",
lastUpdated: true,
themeConfig: {
nav: [
{ text: "首页", link: "/" },
{ text: "指南", link: "/guide/getting-started" },
],
sidebar: {
"/guide/": [
{
text: "指南",
items: [
{ text: "快速开始", link: "/guide/getting-started" },
],
},
],
},
search: {
provider: "local",
},
footer: {
message: "Powered by VitePress-CMS",
copyright: "Copyright 2026",
},
},
});
`;
try {
packageJson = JSON.parse(existingSource);
} catch {
throw new Error("package.json is not valid JSON");
}
packageJson.type = "module";
packageJson.scripts = {
...packageJson.scripts,
"docs:dev": `vitepress dev ${root}`,
"docs:build": `vitepress build ${root}`,
"docs:preview": `vitepress preview ${root}`,
};
packageJson.devDependencies = {
...packageJson.devDependencies,
vitepress: packageJson.devDependencies?.vitepress || "^1.6.4",
};
return `${JSON.stringify(packageJson, null, 2)}\n`;
}
async function loadVitePressScaffold() {
try {
const module = await import("vitepress");
return module.scaffold;
} catch {
const module = await import("../test-vitepress/node_modules/vitepress/dist/node/index.js");
return module.scaffold;
}
}
async function createOfficialVitePressFiles(siteRoot) {
const tempRoot = mkdtempSync(join(tmpdir(), "vitepress-cms-init-"));
const cwd = process.cwd();
try {
const scaffold = await loadVitePressScaffold();
writeFileSync(join(tempRoot, "package.json"), packageJsonTemplate(siteRoot), "utf-8");
process.chdir(tempRoot);
scaffold({
root: siteRoot,
title: "VitePress Site",
description: "Created by VitePress-CMS",
theme: "default theme",
useTs: true,
injectNpmScripts: true,
});
const configPath = [
toRepoPath(siteRoot, ".vitepress/config.ts"),
toRepoPath(siteRoot, ".vitepress/config.mts"),
toRepoPath(siteRoot, ".vitepress/config.mjs"),
toRepoPath(siteRoot, ".vitepress/config.js"),
].find((path) => {
try {
readFileSync(join(tempRoot, path), "utf-8");
return true;
} catch {
return false;
}
});
if (!configPath) {
throw new Error("VitePress official scaffold did not generate a config file");
}
const filePaths = [
"package.json",
toRepoPath(siteRoot, "index.md"),
toRepoPath(siteRoot, "api-examples.md"),
toRepoPath(siteRoot, "markdown-examples.md"),
configPath,
];
return filePaths.map((path) => ({
path,
content:
path === "package.json"
? mergePackageJsonContent(readFileSync(join(tempRoot, path), "utf-8"), siteRoot)
: readFileSync(join(tempRoot, path), "utf-8"),
}));
} finally {
process.chdir(cwd);
rmSync(tempRoot, { recursive: true, force: true });
}
}
function deployWorkflowTemplate(siteRoot, branch) {
const root = normalizeSiteRoot(siteRoot);
const outputDir = root ? `${root}/.vitepress/dist` : ".vitepress/dist";
const root = getVitePressSiteRoot(siteRoot);
const outputDir = `${root}/.vitepress/dist`;
return `name: Deploy VitePress site to Pages
@@ -579,7 +659,7 @@ permissions:
concurrency:
group: pages
cancel-in-progress: false
cancel-in-progress: true
jobs:
build:
@@ -603,44 +683,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
`;
}
function initTemplateFiles(siteRoot, branch) {
async function initTemplateFiles(siteRoot, branch) {
const root = getVitePressSiteRoot(siteRoot);
const files = await createOfficialVitePressFiles(root);
return [
{
path: "package.json",
content: packageJsonTemplate(siteRoot),
},
{
path: toRepoPath(siteRoot, ".vitepress/config.ts"),
content: siteConfigTemplate(),
},
{
path: toRepoPath(siteRoot, "index.md"),
content: `---
title: 首页
description: VitePress-CMS 初始化页面
---
# VitePress Site
这个站点由 VitePress-CMS 初始化。
`,
},
{
path: toRepoPath(siteRoot, "guide/getting-started.md"),
content: `---
title: 快速开始
description: 使用 VitePress-CMS 管理内容
---
# 快速开始
这里是你的第一篇指南文档。
`,
},
...files,
{
path: ".github/workflows/deploy.yml",
content: deployWorkflowTemplate(siteRoot, branch),
@@ -759,7 +811,7 @@ function getSiteQuery(url) {
const owner = url.searchParams.get("owner");
const repo = url.searchParams.get("repo");
const branch = url.searchParams.get("branch") || "main";
const siteRoot = url.searchParams.get("siteRoot") || "";
const siteRoot = getVitePressSiteRoot(url.searchParams.get("siteRoot") || "");
if (!owner || !repo) {
throw new Error("owner and repo are required");
@@ -789,6 +841,28 @@ async function createRepoPathFile(session, site, file) {
try {
const existing = await getRepoPathContent(session, site, file.path);
if (file.path === "package.json") {
const encodedPath = file.path.split("/").map(encodeURIComponent).join("/");
const result = await githubFetch(session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "docs: update package metadata from VitePress-CMS",
content: encodeBase64Content(mergePackageJsonContent(decodeBase64Content(existing.content), site.siteRoot)),
branch: site.branch,
sha: existing.sha,
}),
});
return {
path: file.path,
status: "updated",
sha: result.content.sha,
};
}
if (file.path.startsWith(".github/workflows/")) {
const encodedPath = file.path.split("/").map(encodeURIComponent).join("/");
const result = await githubFetch(session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}`, {
@@ -891,11 +965,11 @@ async function handleInitSite(request, response) {
owner: String(site.owner),
repo: String(site.repo),
branch: String(site.branch || "main"),
siteRoot: normalizeSiteRoot(String(site.siteRoot || "")),
siteRoot: getVitePressSiteRoot(String(site.siteRoot || "")),
};
const files = [];
for (const file of initTemplateFiles(normalizedSite.siteRoot, normalizedSite.branch)) {
for (const file of await initTemplateFiles(normalizedSite.siteRoot, normalizedSite.branch)) {
files.push(await createRepoPathFile(session, normalizedSite, file));
}