功能:完善用户与项目管理框架
This commit is contained in:
+149
-75
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user