@polardbx/pxbaas 0.0.0-stage → 0.1.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -2
- package/bin/pxbaas.js +13 -0
- package/dist/app/local-index.d.ts +70 -0
- package/dist/app/local-index.d.ts.map +1 -0
- package/dist/app/local-index.js +140 -0
- package/dist/app/local-index.js.map +1 -0
- package/dist/app/preflight.d.ts +61 -0
- package/dist/app/preflight.d.ts.map +1 -0
- package/dist/app/preflight.js +115 -0
- package/dist/app/preflight.js.map +1 -0
- package/dist/app/spec-lint.d.ts +45 -0
- package/dist/app/spec-lint.d.ts.map +1 -0
- package/dist/app/spec-lint.js +423 -0
- package/dist/app/spec-lint.js.map +1 -0
- package/dist/app/templates.d.ts +167 -0
- package/dist/app/templates.d.ts.map +1 -0
- package/dist/app/templates.js +151 -0
- package/dist/app/templates.js.map +1 -0
- package/dist/cli/help-guides.d.ts +17 -0
- package/dist/cli/help-guides.d.ts.map +1 -0
- package/dist/cli/help-guides.js +132 -0
- package/dist/cli/help-guides.js.map +1 -0
- package/dist/cli/root.d.ts +3 -0
- package/dist/cli/root.d.ts.map +1 -0
- package/dist/cli/root.js +75 -0
- package/dist/cli/root.js.map +1 -0
- package/dist/commands/app/index.d.ts +8 -0
- package/dist/commands/app/index.d.ts.map +1 -0
- package/dist/commands/app/index.js +1907 -0
- package/dist/commands/app/index.js.map +1 -0
- package/dist/commands/auth/index.d.ts +3 -0
- package/dist/commands/auth/index.d.ts.map +1 -0
- package/dist/commands/auth/index.js +198 -0
- package/dist/commands/auth/index.js.map +1 -0
- package/dist/commands/completion/index.d.ts +3 -0
- package/dist/commands/completion/index.d.ts.map +1 -0
- package/dist/commands/completion/index.js +14 -0
- package/dist/commands/completion/index.js.map +1 -0
- package/dist/commands/data/index.d.ts +55 -0
- package/dist/commands/data/index.d.ts.map +1 -0
- package/dist/commands/data/index.js +407 -0
- package/dist/commands/data/index.js.map +1 -0
- package/dist/commands/db/index.d.ts +29 -0
- package/dist/commands/db/index.d.ts.map +1 -0
- package/dist/commands/db/index.js +500 -0
- package/dist/commands/db/index.js.map +1 -0
- package/dist/commands/doctor/index.d.ts +3 -0
- package/dist/commands/doctor/index.d.ts.map +1 -0
- package/dist/commands/doctor/index.js +305 -0
- package/dist/commands/doctor/index.js.map +1 -0
- package/dist/commands/functions/delete.d.ts +3 -0
- package/dist/commands/functions/delete.d.ts.map +1 -0
- package/dist/commands/functions/delete.js +55 -0
- package/dist/commands/functions/delete.js.map +1 -0
- package/dist/commands/functions/deploy.d.ts +3 -0
- package/dist/commands/functions/deploy.d.ts.map +1 -0
- package/dist/commands/functions/deploy.js +118 -0
- package/dist/commands/functions/deploy.js.map +1 -0
- package/dist/commands/functions/index.d.ts +3 -0
- package/dist/commands/functions/index.d.ts.map +1 -0
- package/dist/commands/functions/index.js +15 -0
- package/dist/commands/functions/index.js.map +1 -0
- package/dist/commands/functions/list.d.ts +6 -0
- package/dist/commands/functions/list.d.ts.map +1 -0
- package/dist/commands/functions/list.js +63 -0
- package/dist/commands/functions/list.js.map +1 -0
- package/dist/commands/functions/new.d.ts +7 -0
- package/dist/commands/functions/new.d.ts.map +1 -0
- package/dist/commands/functions/new.js +72 -0
- package/dist/commands/functions/new.js.map +1 -0
- package/dist/commands/gen/index.d.ts +3 -0
- package/dist/commands/gen/index.d.ts.map +1 -0
- package/dist/commands/gen/index.js +323 -0
- package/dist/commands/gen/index.js.map +1 -0
- package/dist/commands/guide/index.d.ts +8 -0
- package/dist/commands/guide/index.d.ts.map +1 -0
- package/dist/commands/guide/index.js +20 -0
- package/dist/commands/guide/index.js.map +1 -0
- package/dist/commands/instance/index.d.ts +8 -0
- package/dist/commands/instance/index.d.ts.map +1 -0
- package/dist/commands/instance/index.js +481 -0
- package/dist/commands/instance/index.js.map +1 -0
- package/dist/commands/login/index.d.ts +3 -0
- package/dist/commands/login/index.d.ts.map +1 -0
- package/dist/commands/login/index.js +145 -0
- package/dist/commands/login/index.js.map +1 -0
- package/dist/commands/logout/index.d.ts +3 -0
- package/dist/commands/logout/index.d.ts.map +1 -0
- package/dist/commands/logout/index.js +41 -0
- package/dist/commands/logout/index.js.map +1 -0
- package/dist/commands/profile/import-env.d.ts +18 -0
- package/dist/commands/profile/import-env.d.ts.map +1 -0
- package/dist/commands/profile/import-env.js +206 -0
- package/dist/commands/profile/import-env.js.map +1 -0
- package/dist/commands/profile/index.d.ts +3 -0
- package/dist/commands/profile/index.d.ts.map +1 -0
- package/dist/commands/profile/index.js +124 -0
- package/dist/commands/profile/index.js.map +1 -0
- package/dist/commands/rest/index.d.ts +3 -0
- package/dist/commands/rest/index.d.ts.map +1 -0
- package/dist/commands/rest/index.js +102 -0
- package/dist/commands/rest/index.js.map +1 -0
- package/dist/commands/skill/index.d.ts +11 -0
- package/dist/commands/skill/index.d.ts.map +1 -0
- package/dist/commands/skill/index.js +228 -0
- package/dist/commands/skill/index.js.map +1 -0
- package/dist/commands/status/index.d.ts +3 -0
- package/dist/commands/status/index.d.ts.map +1 -0
- package/dist/commands/status/index.js +59 -0
- package/dist/commands/status/index.js.map +1 -0
- package/dist/commands/storage/index.d.ts +3 -0
- package/dist/commands/storage/index.d.ts.map +1 -0
- package/dist/commands/storage/index.js +400 -0
- package/dist/commands/storage/index.js.map +1 -0
- package/dist/commands/tenant/index.d.ts +19 -0
- package/dist/commands/tenant/index.d.ts.map +1 -0
- package/dist/commands/tenant/index.js +667 -0
- package/dist/commands/tenant/index.js.map +1 -0
- package/dist/commands/whoami/index.d.ts +3 -0
- package/dist/commands/whoami/index.d.ts.map +1 -0
- package/dist/commands/whoami/index.js +101 -0
- package/dist/commands/whoami/index.js.map +1 -0
- package/dist/credentials/commit.d.ts +38 -0
- package/dist/credentials/commit.d.ts.map +1 -0
- package/dist/credentials/commit.js +57 -0
- package/dist/credentials/commit.js.map +1 -0
- package/dist/credentials/credential-store.d.ts +86 -0
- package/dist/credentials/credential-store.d.ts.map +1 -0
- package/dist/credentials/credential-store.js +282 -0
- package/dist/credentials/credential-store.js.map +1 -0
- package/dist/credentials/env-file.d.ts +85 -0
- package/dist/credentials/env-file.d.ts.map +1 -0
- package/dist/credentials/env-file.js +193 -0
- package/dist/credentials/env-file.js.map +1 -0
- package/dist/credentials/formatter.d.ts +39 -0
- package/dist/credentials/formatter.d.ts.map +1 -0
- package/dist/credentials/formatter.js +79 -0
- package/dist/credentials/formatter.js.map +1 -0
- package/dist/credentials/index.d.ts +13 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +13 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/jwt-introspect.d.ts +39 -0
- package/dist/credentials/jwt-introspect.d.ts.map +1 -0
- package/dist/credentials/jwt-introspect.js +64 -0
- package/dist/credentials/jwt-introspect.js.map +1 -0
- package/dist/credentials/masker.d.ts +23 -0
- package/dist/credentials/masker.d.ts.map +1 -0
- package/dist/credentials/masker.js +40 -0
- package/dist/credentials/masker.js.map +1 -0
- package/dist/credentials/profile.d.ts +95 -0
- package/dist/credentials/profile.d.ts.map +1 -0
- package/dist/credentials/profile.js +207 -0
- package/dist/credentials/profile.js.map +1 -0
- package/dist/credentials/tenant-guard.d.ts +16 -0
- package/dist/credentials/tenant-guard.d.ts.map +1 -0
- package/dist/credentials/tenant-guard.js +53 -0
- package/dist/credentials/tenant-guard.js.map +1 -0
- package/dist/credentials/tty-guard.d.ts +29 -0
- package/dist/credentials/tty-guard.d.ts.map +1 -0
- package/dist/credentials/tty-guard.js +79 -0
- package/dist/credentials/tty-guard.js.map +1 -0
- package/dist/db/apply-schema.d.ts +144 -0
- package/dist/db/apply-schema.d.ts.map +1 -0
- package/dist/db/apply-schema.js +359 -0
- package/dist/db/apply-schema.js.map +1 -0
- package/dist/db/introspect.d.ts +54 -0
- package/dist/db/introspect.d.ts.map +1 -0
- package/dist/db/introspect.js +140 -0
- package/dist/db/introspect.js.map +1 -0
- package/dist/db/sql-file.d.ts +104 -0
- package/dist/db/sql-file.d.ts.map +1 -0
- package/dist/db/sql-file.js +584 -0
- package/dist/db/sql-file.js.map +1 -0
- package/dist/instance/provision.d.ts +89 -0
- package/dist/instance/provision.d.ts.map +1 -0
- package/dist/instance/provision.js +139 -0
- package/dist/instance/provision.js.map +1 -0
- package/dist/instance/zero-store.d.ts +47 -0
- package/dist/instance/zero-store.d.ts.map +1 -0
- package/dist/instance/zero-store.js +91 -0
- package/dist/instance/zero-store.js.map +1 -0
- package/dist/instance/zero-whitelist.d.ts +39 -0
- package/dist/instance/zero-whitelist.d.ts.map +1 -0
- package/dist/instance/zero-whitelist.js +67 -0
- package/dist/instance/zero-whitelist.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +69 -0
- package/dist/main.js.map +1 -0
- package/dist/skill/agents-md.d.ts +25 -0
- package/dist/skill/agents-md.d.ts.map +1 -0
- package/dist/skill/agents-md.js +62 -0
- package/dist/skill/agents-md.js.map +1 -0
- package/dist/skill/assets.d.ts +38 -0
- package/dist/skill/assets.d.ts.map +1 -0
- package/dist/skill/assets.js +111 -0
- package/dist/skill/assets.js.map +1 -0
- package/dist/skill/hint.d.ts +39 -0
- package/dist/skill/hint.d.ts.map +1 -0
- package/dist/skill/hint.js +109 -0
- package/dist/skill/hint.js.map +1 -0
- package/dist/skill/hosts.d.ts +45 -0
- package/dist/skill/hosts.d.ts.map +1 -0
- package/dist/skill/hosts.js +96 -0
- package/dist/skill/hosts.js.map +1 -0
- package/dist/skill/installer.d.ts +100 -0
- package/dist/skill/installer.d.ts.map +1 -0
- package/dist/skill/installer.js +241 -0
- package/dist/skill/installer.js.map +1 -0
- package/dist/skill/ownership.d.ts +35 -0
- package/dist/skill/ownership.d.ts.map +1 -0
- package/dist/skill/ownership.js +52 -0
- package/dist/skill/ownership.js.map +1 -0
- package/dist/tenant/bundle.d.ts +66 -0
- package/dist/tenant/bundle.d.ts.map +1 -0
- package/dist/tenant/bundle.js +132 -0
- package/dist/tenant/bundle.js.map +1 -0
- package/dist/tenant/guide.d.ts +50 -0
- package/dist/tenant/guide.d.ts.map +1 -0
- package/dist/tenant/guide.js +152 -0
- package/dist/tenant/guide.js.map +1 -0
- package/dist/tenant/probe.d.ts +65 -0
- package/dist/tenant/probe.d.ts.map +1 -0
- package/dist/tenant/probe.js +159 -0
- package/dist/tenant/probe.js.map +1 -0
- package/dist/tenant/verify.d.ts +37 -0
- package/dist/tenant/verify.d.ts.map +1 -0
- package/dist/tenant/verify.js +132 -0
- package/dist/tenant/verify.js.map +1 -0
- package/dist/version.d.ts +29 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +65 -0
- package/dist/version.js.map +1 -0
- package/package.json +63 -3
- package/skills/pxbaas-app-spec/SKILL.md +165 -0
- package/skills/pxbaas-app-spec/references/deploy-options.md +158 -0
- package/skills/pxbaas-app-spec/references/guardrails-pitfalls.md +89 -0
- package/skills/pxbaas-app-spec/references/runtime-contract.md +151 -0
- package/skills/pxbaas-app-spec/references/ssr-session.md +121 -0
- package/skills/pxbaas-app-spec/references/templates-examples.md +311 -0
- package/skills/pxbaas-cli/SKILL.md +186 -0
- package/skills/pxbaas-cli/references/api-reference.md +68 -0
- package/skills/pxbaas-cli/references/app-deploy.md +186 -0
- package/skills/pxbaas-cli/references/auth-and-credentials.md +116 -0
- package/skills/pxbaas-cli/references/data-postgrest.md +101 -0
- package/skills/pxbaas-cli/references/db-and-meta.md +102 -0
- package/skills/pxbaas-cli/references/edge-functions.md +71 -0
- package/skills/pxbaas-cli/references/multi-tenant-apps.md +138 -0
- package/skills/pxbaas-cli/references/provision.md +101 -0
- package/skills/pxbaas-cli/references/safety-and-two-phase.md +78 -0
- package/skills/pxbaas-cli/references/storage.md +61 -0
- package/skills/pxbaas-cli/references/tenant-admin.md +122 -0
- package/skills/pxbaas-cli/references/troubleshooting.md +76 -0
- package/templates/webapp/api/index.ts +55 -0
- package/templates/webapp/csr-bff/README.md +81 -0
- package/templates/webapp/csr-bff/backend/index.ts +168 -0
- package/templates/webapp/csr-bff/db/rls.json +13 -0
- package/templates/webapp/csr-bff/db/schema.sql +49 -0
- package/templates/webapp/csr-bff/frontend/dist/admin.html +101 -0
- package/templates/webapp/csr-bff/frontend/dist/article.html +44 -0
- package/templates/webapp/csr-bff/frontend/dist/blog.js +206 -0
- package/templates/webapp/csr-bff/frontend/dist/blogger.html +53 -0
- package/templates/webapp/csr-bff/frontend/dist/index.html +70 -0
- package/templates/webapp/csr-bff/frontend/dist/login.html +56 -0
- package/templates/webapp/csr-bff/frontend/dist/profile.html +59 -0
- package/templates/webapp/csr-bff/frontend/dist/signup.html +78 -0
- package/templates/webapp/csr-bff/frontend/dist/style.css +27 -0
- package/templates/webapp/manifest.json +419 -0
- package/templates/webapp/mpa/about.html +29 -0
- package/templates/webapp/mpa/app.js +42 -0
- package/templates/webapp/mpa/index.html +42 -0
- package/templates/webapp/mpa/index.ts +60 -0
- package/templates/webapp/mpa/style.css +42 -0
- package/templates/webapp/react-vite/frontend/index.html +12 -0
- package/templates/webapp/react-vite/frontend/package-lock.json +1826 -0
- package/templates/webapp/react-vite/frontend/package.json +23 -0
- package/templates/webapp/react-vite/frontend/src/App.tsx +34 -0
- package/templates/webapp/react-vite/frontend/src/main.tsx +13 -0
- package/templates/webapp/react-vite/frontend/src/style.css +46 -0
- package/templates/webapp/react-vite/frontend/tsconfig.json +20 -0
- package/templates/webapp/react-vite/frontend/vite.config.ts +10 -0
- package/templates/webapp/react-vite-hono/backend/index.ts +38 -0
- package/templates/webapp/react-vite-hono/frontend/index.html +12 -0
- package/templates/webapp/react-vite-hono/frontend/package-lock.json +1826 -0
- package/templates/webapp/react-vite-hono/frontend/package.json +23 -0
- package/templates/webapp/react-vite-hono/frontend/src/App.tsx +47 -0
- package/templates/webapp/react-vite-hono/frontend/src/main.tsx +13 -0
- package/templates/webapp/react-vite-hono/frontend/src/style.css +66 -0
- package/templates/webapp/react-vite-hono/frontend/tsconfig.json +20 -0
- package/templates/webapp/react-vite-hono/frontend/vite.config.ts +10 -0
- package/templates/webapp/spa/index.ts +119 -0
- package/templates/webapp/ssr-auth/backend/index.ts +133 -0
- package/templates/webapp/ssr-auth/frontend/dist/index.html +15 -0
- package/templates/webapp/ssr-blog/README.md +111 -0
- package/templates/webapp/ssr-blog/backend/index.ts +764 -0
- package/templates/webapp/ssr-blog/db/rls.json +13 -0
- package/templates/webapp/ssr-blog/db/schema.sql +51 -0
- package/templates/webapp/ssr-blog/frontend/dist/index.html +39 -0
- package/templates/webapp/ssr-frameworks/README.md +64 -0
- package/templates/webapp/ssr-frameworks/backend/index.ts +241 -0
- package/templates/webapp/ssr-frameworks/frontend/dist/index.html +31 -0
- package/templates/webapp/ssr-hono/backend/index.ts +111 -0
- package/templates/webapp/ssr-hono/frontend/dist/index.html +12 -0
- package/templates/webapp/ssr-shop/README.md +80 -0
- package/templates/webapp/ssr-shop/backend/index.ts +491 -0
- package/templates/webapp/ssr-shop/db/rls.json +16 -0
- package/templates/webapp/ssr-shop/db/schema.sql +90 -0
- package/templates/webapp/ssr-shop/frontend/dist/index.html +12 -0
- package/templates/webapp/vanilla-static/frontend/dist/app.js +6 -0
- package/templates/webapp/vanilla-static/frontend/dist/index.html +19 -0
- package/templates/webapp/vanilla-static/frontend/dist/style.css +43 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>MPA Blog — 博主管理</title>
|
|
7
|
+
<link rel="stylesheet" href="./style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<nav><a href="./index.html">← 返回广场</a><a href="./profile.html">我的资料</a><span class="spacer"></span><span id="who" class="muted"></span></nav>
|
|
11
|
+
<div id="gate"></div>
|
|
12
|
+
<div id="admin" style="display:none">
|
|
13
|
+
<h1>写文章</h1>
|
|
14
|
+
<div class="card">
|
|
15
|
+
<input id="title" placeholder="标题" maxlength="200">
|
|
16
|
+
<textarea id="content" rows="5" placeholder="正文(text/markdown,渲染时转义)"></textarea>
|
|
17
|
+
<select id="status"><option value="draft">draft(草稿)</option><option value="published">published(发布)</option></select>
|
|
18
|
+
<button id="create">创建</button>
|
|
19
|
+
<div id="msg" class="muted"></div>
|
|
20
|
+
</div>
|
|
21
|
+
<h1>我的文章</h1>
|
|
22
|
+
<div id="list"><div class="muted">加载中…</div></div>
|
|
23
|
+
</div>
|
|
24
|
+
<script src="./blog.js"></script>
|
|
25
|
+
<script>
|
|
26
|
+
var gate = document.getElementById('gate');
|
|
27
|
+
var user = null;
|
|
28
|
+
BLOG.init().then(function () {
|
|
29
|
+
if (BLOG.isSharedView()) {
|
|
30
|
+
gate.innerHTML = '<div class="banner">分享只读视图:管理功能已禁用。请通过函数原始 URL 打开本页进行管理。</div>' +
|
|
31
|
+
'<div class="muted">下面仅展示公开可见文章。</div>';
|
|
32
|
+
return BLOG.loadPosts().then(renderList);
|
|
33
|
+
}
|
|
34
|
+
return BLOG.me().then(function (m) {
|
|
35
|
+
if (!m.user) { location.replace('./login.html'); return; }
|
|
36
|
+
user = m.user;
|
|
37
|
+
document.getElementById('who').textContent = user.email || user.id;
|
|
38
|
+
document.getElementById('admin').style.display = '';
|
|
39
|
+
document.getElementById('create').onclick = create;
|
|
40
|
+
// 管理列表仅展示本人文章(他人已发布文章在首页/分享视图可见,RLS 服务端强制)
|
|
41
|
+
return BLOG.loadPosts().then(function (rows) {
|
|
42
|
+
renderList(rows.filter(function (p) { return p.author_id === user.id; }));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}).catch(function (e) {
|
|
46
|
+
gate.innerHTML = '<div class="error">初始化失败: ' + BLOG.esc(e) + '</div>';
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function renderList(rows) {
|
|
50
|
+
var el = document.getElementById('list');
|
|
51
|
+
if (!rows.length) { el.innerHTML = '<div class="muted">暂无文章。</div>'; return; }
|
|
52
|
+
el.innerHTML = rows.map(function (p) {
|
|
53
|
+
var ops = '';
|
|
54
|
+
if (user) {
|
|
55
|
+
ops = '<span>' +
|
|
56
|
+
(p.status === 'draft'
|
|
57
|
+
? '<button class="ghost" data-act="publish" data-id="' + p.id + '">发布</button> '
|
|
58
|
+
: '<button class="ghost" data-act="unpublish" data-id="' + p.id + '">转草稿</button> ') +
|
|
59
|
+
'<button class="danger" data-act="delete" data-id="' + p.id + '">删除</button></span>';
|
|
60
|
+
}
|
|
61
|
+
return '<div class="card"><div class="row"><h2><a href="./article.html#id=' + encodeURIComponent(p.id) + '">' +
|
|
62
|
+
BLOG.esc(p.title) + '</a></h2><span class="badge ' + BLOG.esc(p.status) + '">' + BLOG.esc(p.status) + '</span></div>' +
|
|
63
|
+
'<div class="row muted"><span>#' + BLOG.esc(p.id) + ' · ' + BLOG.esc(BLOG.fmtDate(p.created_at)) + '</span>' + ops + '</div></div>';
|
|
64
|
+
}).join('');
|
|
65
|
+
Array.prototype.forEach.call(el.querySelectorAll('button[data-act]'), function (b) {
|
|
66
|
+
b.onclick = function () { act(b.getAttribute('data-act'), b.getAttribute('data-id')); };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function create() {
|
|
71
|
+
var msg = document.getElementById('msg');
|
|
72
|
+
var body = {
|
|
73
|
+
title: document.getElementById('title').value.trim(),
|
|
74
|
+
content: document.getElementById('content').value,
|
|
75
|
+
status: document.getElementById('status').value,
|
|
76
|
+
author_id: user.id,
|
|
77
|
+
author_name: BLOG.displayName() || String(user.email || '').split('@')[0] || '匿名博主',
|
|
78
|
+
};
|
|
79
|
+
if (!body.title) { msg.className = 'error'; msg.textContent = '标题不能为空'; return; }
|
|
80
|
+
fetch(BLOG.rest(), { method: 'POST', headers: BLOG.headers(true), body: JSON.stringify(body) })
|
|
81
|
+
.then(function (r) {
|
|
82
|
+
if (!r.ok) return r.text().then(function (t) { throw new Error('HTTP ' + r.status + ' ' + t); });
|
|
83
|
+
msg.className = 'muted'; msg.textContent = '已创建';
|
|
84
|
+
document.getElementById('title').value = ''; document.getElementById('content').value = '';
|
|
85
|
+
return BLOG.loadPosts().then(renderList);
|
|
86
|
+
}).catch(function (e) { msg.className = 'error'; msg.textContent = String(e); });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function act(kind, id) {
|
|
90
|
+
var patch = kind === 'publish' ? { status: 'published' } : kind === 'unpublish' ? { status: 'draft' } : null;
|
|
91
|
+
var p = patch
|
|
92
|
+
? fetch(BLOG.rest() + '?id=eq.' + encodeURIComponent(id), { method: 'PATCH', headers: BLOG.headers(true), body: JSON.stringify(patch) })
|
|
93
|
+
: fetch(BLOG.rest() + '?id=eq.' + encodeURIComponent(id), { method: 'DELETE', headers: BLOG.headers(true) });
|
|
94
|
+
p.then(function (r) {
|
|
95
|
+
if (!r.ok) return r.text().then(function (t) { throw new Error('HTTP ' + r.status + ' ' + t); });
|
|
96
|
+
return BLOG.loadPosts().then(renderList);
|
|
97
|
+
}).catch(function (e) { alert('操作失败: ' + e); });
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>MPA Blog — 文章详情</title>
|
|
7
|
+
<link rel="stylesheet" href="./style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<nav><a href="./index.html">← 返回广场</a><span class="spacer"></span><button id="copylink" class="ghost">复制分享链接</button></nav>
|
|
11
|
+
<div id="copymsg" class="muted"></div>
|
|
12
|
+
<div id="view"><div class="muted">加载中…</div></div>
|
|
13
|
+
<script src="./blog.js"></script>
|
|
14
|
+
<script>
|
|
15
|
+
document.getElementById('copylink').onclick = function () {
|
|
16
|
+
var msg = document.getElementById('copymsg');
|
|
17
|
+
BLOG.copyText(location.href).then(function (ok) {
|
|
18
|
+
msg.textContent = ok ? '已复制:' + location.href : '复制失败,请手动复制地址栏:' + location.href;
|
|
19
|
+
if (!ok) prompt('复制分享链接:', location.href);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var raw = location.hash.replace(/^#/, '');
|
|
23
|
+
var id = raw.indexOf('id=') === 0 ? raw.slice(3) : raw;
|
|
24
|
+
var el = document.getElementById('view');
|
|
25
|
+
if (!/^\d+$/.test(id)) {
|
|
26
|
+
el.innerHTML = '<div class="error">缺少文章 ID(正确形式:article.html#id=1)</div>';
|
|
27
|
+
} else {
|
|
28
|
+
BLOG.init().then(function () { return BLOG.loadPost(id); }).then(function (p) {
|
|
29
|
+
if (!p) { el.innerHTML = '<div class="error">文章不存在或不可见(未发布且非作者)。</div>'; return; }
|
|
30
|
+
if (BLOG.isSharedView() && p.status !== 'published') {
|
|
31
|
+
el.innerHTML = '<div class="error">该文章尚未发布,分享只读视图不可见。</div>'; return;
|
|
32
|
+
}
|
|
33
|
+
el.innerHTML =
|
|
34
|
+
'<h1>' + BLOG.esc(p.title) + ' <span class="badge ' + BLOG.esc(p.status) + '">' + BLOG.esc(p.status) + '</span></h1>' +
|
|
35
|
+
'<div class="muted">博主: ' + BLOG.esc(BLOG.authorLabel(p)) + ' · #' + BLOG.esc(p.id) + ' · ' +
|
|
36
|
+
BLOG.esc(BLOG.fmtDate(p.created_at)) + '</div>' +
|
|
37
|
+
'<p class="content">' + BLOG.esc(p.content || '') + '</p>';
|
|
38
|
+
}).catch(function (e) {
|
|
39
|
+
el.innerHTML = '<div class="error">加载失败: ' + BLOG.esc(e) + '</div>';
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/* mpa-blog shared client helpers. All page-internal links are relative.
|
|
2
|
+
前后端分离(BFF):数据/认证一律走相对 ./api/*,由 sandbox worker 代理到内网
|
|
3
|
+
/rest/v1 与 /auth/v1;浏览器不再直连 Supabase,也不持有 anon key 之外的凭据。 */
|
|
4
|
+
(function () {
|
|
5
|
+
var state = { anon: "" };
|
|
6
|
+
|
|
7
|
+
function esc(s) {
|
|
8
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
|
9
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function token() { return localStorage.getItem("blog_jwt") || ""; }
|
|
13
|
+
function setToken(t) { localStorage.setItem("blog_jwt", t); }
|
|
14
|
+
function clearToken() { localStorage.removeItem("blog_jwt"); proxySession("logout", null); }
|
|
15
|
+
function rest() { return "./api/rest/posts"; }
|
|
16
|
+
function authBase() { return "./api/auth"; }
|
|
17
|
+
|
|
18
|
+
// /apps/{mount}/ 入口下平台会剥离浏览器的 Authorization(TRUSTED_INTERNAL),
|
|
19
|
+
// BFF 的“JWT 透传”在那里拿不到用户身份;改由 proxy 校验 per-mount 会话 cookie
|
|
20
|
+
// 后注入可信身份头。故登录/注册成功后顺带建立该 cookie,登出时清除。
|
|
21
|
+
// 端点不存在时(旧实例或 /functions 入口)静默忽略,保留原有 localStorage 行为。
|
|
22
|
+
function proxySession(action, body) {
|
|
23
|
+
try {
|
|
24
|
+
return fetch("./_session/" + action, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: body ? { "Content-Type": "application/json" } : {},
|
|
27
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
28
|
+
credentials: "same-origin",
|
|
29
|
+
keepalive: true,
|
|
30
|
+
}).catch(function () { return null; });
|
|
31
|
+
} catch (e) { return Promise.resolve(null); }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 拉取 anon 配置;worker 冷启动(部署后沙箱 resume)期间可能返回非 anon 响应,
|
|
35
|
+
// 这里有限重试,避免 anon 为空导致后续 REST 请求 401。
|
|
36
|
+
function fetchConfig(attempt) {
|
|
37
|
+
return fetch("./api/config")
|
|
38
|
+
.then(function (r) { return r.json().catch(function () { return {}; }); })
|
|
39
|
+
.then(function (j) {
|
|
40
|
+
if (j && j.anon) { state.anon = j.anon; return state.anon; }
|
|
41
|
+
if (attempt > 0) return delay(2000).then(function () { return fetchConfig(attempt - 1); });
|
|
42
|
+
return "";
|
|
43
|
+
})
|
|
44
|
+
.catch(function () {
|
|
45
|
+
if (attempt > 0) return delay(2000).then(function () { return fetchConfig(attempt - 1); });
|
|
46
|
+
return "";
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function delay(ms) { return new Promise(function (res) { setTimeout(res, ms); }); }
|
|
50
|
+
|
|
51
|
+
function init() {
|
|
52
|
+
return fetchConfig(4);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function headers(withAuth) {
|
|
56
|
+
var h = { "apikey": state.anon, "Content-Type": "application/json" };
|
|
57
|
+
// 分享只读视图必须呈现纯公开(anon)数据:不携带本地登录态,
|
|
58
|
+
// 否则 authenticated 的「published 或本人草稿」规则会把查看者自己的草稿混入公开视图。
|
|
59
|
+
if (withAuth && token() && !isSharedView()) h["Authorization"] = "Bearer " + token();
|
|
60
|
+
return h;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function me() {
|
|
64
|
+
// 会话判定经 BFF worker 代理到内网 GoTrue(./api/auth/user):worker 转发本请求的
|
|
65
|
+
// Authorization,由 GoTrue 服务端校验会话,浏览器不再直连 /auth/v1。
|
|
66
|
+
if (!token()) return Promise.resolve({ user: null });
|
|
67
|
+
return fetch(authBase() + "/user", {
|
|
68
|
+
headers: { "apikey": state.anon, "Authorization": "Bearer " + token() },
|
|
69
|
+
})
|
|
70
|
+
.then(function (r) {
|
|
71
|
+
// 本地 token 失效(过期/密钥轮换/跨实例)时立即清除:否则坏 token 会被
|
|
72
|
+
// headers(true) 继续附到公开 REST 请求上,导致游客视图整体 401。
|
|
73
|
+
if (!r.ok) { clearToken(); return null; }
|
|
74
|
+
return r.json();
|
|
75
|
+
})
|
|
76
|
+
.then(function (u) { return { user: u && u.id ? u : null }; })
|
|
77
|
+
.catch(function () { return { user: null }; });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 带登录态读 REST;若服务端拒签本地 token(401),清除坏 token 并回退纯 anon 重试一次,
|
|
81
|
+
// 保证公开视图不因陈旧登录态而整体失败(自愈)。
|
|
82
|
+
function restRead(url) {
|
|
83
|
+
return fetch(url, { headers: headers(true) }).then(function (r) {
|
|
84
|
+
if (r.status === 401 && token() && !isSharedView()) {
|
|
85
|
+
clearToken();
|
|
86
|
+
return fetch(url, { headers: headers(false) });
|
|
87
|
+
}
|
|
88
|
+
return r;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function restJson(r) {
|
|
92
|
+
if (!r.ok) throw new Error("REST " + r.status);
|
|
93
|
+
return r.json();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadPosts() {
|
|
97
|
+
return restRead(rest() + "?select=id,title,content,status,author_id,author_name,created_at&order=created_at.desc&limit=200")
|
|
98
|
+
.then(restJson);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function loadPost(id) {
|
|
102
|
+
return restRead(rest() + "?id=eq." + encodeURIComponent(id) + "&select=id,title,content,status,author_id,author_name,created_at")
|
|
103
|
+
.then(restJson).then(function (rows) { return rows && rows.length ? rows[0] : null; });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 只看某个博主(author_id)的可见文章;游客视角下 RLS 仅返回其已发布文章。
|
|
107
|
+
function loadPostsByAuthor(authorId) {
|
|
108
|
+
return restRead(rest() + "?author_id=eq." + encodeURIComponent(authorId) +
|
|
109
|
+
"&select=id,title,content,status,author_id,author_name,created_at&order=created_at.desc&limit=200")
|
|
110
|
+
.then(restJson);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 从一组文章里按 author_id 聚合出博主列表:每个博主取其最新 1~2 篇、署名与总数。
|
|
114
|
+
// 入参需已按 created_at desc 排序;署名取该博主最近一篇非空 author_name。
|
|
115
|
+
function groupByAuthor(rows) {
|
|
116
|
+
var order = [];
|
|
117
|
+
var map = {};
|
|
118
|
+
(rows || []).forEach(function (p) {
|
|
119
|
+
var aid = p.author_id || "";
|
|
120
|
+
if (!map[aid]) {
|
|
121
|
+
map[aid] = { author_id: aid, author_name: "", posts: [], count: 0, latest: p.created_at };
|
|
122
|
+
order.push(aid);
|
|
123
|
+
}
|
|
124
|
+
var g = map[aid];
|
|
125
|
+
g.posts.push(p);
|
|
126
|
+
g.count += 1;
|
|
127
|
+
if (!g.author_name && p.author_name) g.author_name = p.author_name;
|
|
128
|
+
});
|
|
129
|
+
return order.map(function (aid) { return map[aid]; });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function signup(email, password) {
|
|
133
|
+
return fetch(authBase() + "/signup", {
|
|
134
|
+
method: "POST", headers: headers(false),
|
|
135
|
+
body: JSON.stringify({ email: email, password: password }),
|
|
136
|
+
}).then(function (r) {
|
|
137
|
+
if (!r.ok) return r;
|
|
138
|
+
return proxySession("login", { email: email, password: password }).then(function () { return r; });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function login(email, password) {
|
|
142
|
+
return fetch(authBase() + "/token?grant_type=password", {
|
|
143
|
+
method: "POST", headers: headers(false),
|
|
144
|
+
body: JSON.stringify({ email: email, password: password }),
|
|
145
|
+
}).then(function (r) {
|
|
146
|
+
if (!r.ok) return r;
|
|
147
|
+
return proxySession("login", { email: email, password: password }).then(function () { return r; });
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function fmtDate(s) { return s ? String(s).replace("T", " ").slice(0, 19) : ""; }
|
|
152
|
+
function isSharedView() { return location.pathname.indexOf("/published/") === 0; }
|
|
153
|
+
|
|
154
|
+
function meta() { try { return JSON.parse(localStorage.getItem("blog_meta") || "{}"); } catch (e) { return {}; } }
|
|
155
|
+
function setMeta(m) { localStorage.setItem("blog_meta", JSON.stringify(m || {})); }
|
|
156
|
+
function displayName() { return meta().display_name || ""; }
|
|
157
|
+
function authorLabel(p) { return p && p.author_name ? p.author_name : "匿名博主"; }
|
|
158
|
+
function getProfile() {
|
|
159
|
+
var h = { "apikey": state.anon };
|
|
160
|
+
if (token()) h["Authorization"] = "Bearer " + token();
|
|
161
|
+
return fetch(authBase() + "/user", { headers: h }).then(function (r) {
|
|
162
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
163
|
+
return r.json();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function updateProfile(data) {
|
|
167
|
+
var h = { "apikey": state.anon, "Content-Type": "application/json" };
|
|
168
|
+
if (token()) h["Authorization"] = "Bearer " + token();
|
|
169
|
+
return fetch(authBase() + "/user", { method: "PUT", headers: h, body: JSON.stringify({ data: data }) })
|
|
170
|
+
.then(function (r) {
|
|
171
|
+
if (!r.ok) return r.json().catch(function () { return {}; }).then(function (j) {
|
|
172
|
+
throw new Error(j.msg || j.error_description || j.error || ("HTTP " + r.status));
|
|
173
|
+
});
|
|
174
|
+
return r.json();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function copyText(t) {
|
|
178
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
179
|
+
return navigator.clipboard.writeText(t).then(function () { return true; }, function () { return false; });
|
|
180
|
+
}
|
|
181
|
+
return Promise.resolve(false);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 把 GoTrue 返回的英文错误映射为中文提示(保留原始文本作为兜底)。
|
|
185
|
+
function authError(j, status) {
|
|
186
|
+
var raw = (j && (j.msg || j.error_description || j.error_description || j.error)) || "";
|
|
187
|
+
if (!raw) return "请求失败(HTTP " + status + ")";
|
|
188
|
+
var s = String(raw).toLowerCase();
|
|
189
|
+
if (s.indexOf("invalid login credentials") >= 0 || s.indexOf("invalid credentials") >= 0) return "邮箱或密码错误";
|
|
190
|
+
if (s.indexOf("email not confirmed") >= 0) return "邮箱未确认,无法登录(该实例通常已开免验证,若出现此提示请联系管理员)";
|
|
191
|
+
if (s.indexOf("already registered") >= 0 || s.indexOf("already exists") >= 0) return "该邮箱已注册,请直接登录";
|
|
192
|
+
if (s.indexOf("invalid email") >= 0 || s.indexOf("unable to validate email") >= 0) return "邮箱格式不正确";
|
|
193
|
+
if (s.indexOf("password") >= 0 && (s.indexOf("character") >= 0 || s.indexOf("short") >= 0)) return "密码长度不足(至少 6 位)";
|
|
194
|
+
if (s.indexOf("rate limit") >= 0 || s.indexOf("too many") >= 0) return "操作过于频繁,请稍后再试";
|
|
195
|
+
return raw;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
window.BLOG = {
|
|
199
|
+
esc: esc, token: token, setToken: setToken, clearToken: clearToken,
|
|
200
|
+
rest: rest, init: init, headers: headers, me: me,
|
|
201
|
+
loadPosts: loadPosts, loadPost: loadPost, loadPostsByAuthor: loadPostsByAuthor, groupByAuthor: groupByAuthor,
|
|
202
|
+
signup: signup, login: login, fmtDate: fmtDate, isSharedView: isSharedView,
|
|
203
|
+
meta: meta, setMeta: setMeta, displayName: displayName, authorLabel: authorLabel,
|
|
204
|
+
getProfile: getProfile, updateProfile: updateProfile, copyText: copyText, authError: authError,
|
|
205
|
+
};
|
|
206
|
+
})();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>MPA Blog — 博主主页</title>
|
|
7
|
+
<link rel="stylesheet" href="./style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<nav><a href="./index.html">← 返回广场</a></nav>
|
|
11
|
+
<div id="banner"></div>
|
|
12
|
+
<h1 id="title">博主主页</h1>
|
|
13
|
+
<div class="muted" id="subtitle" style="margin-bottom:8px">加载中…</div>
|
|
14
|
+
<div id="list"><div class="muted">加载中…</div></div>
|
|
15
|
+
<script src="./blog.js"></script>
|
|
16
|
+
<script>
|
|
17
|
+
var raw = location.hash.replace(/^#/, '');
|
|
18
|
+
var authorId = raw.indexOf('id=') === 0 ? raw.slice(3) : raw;
|
|
19
|
+
authorId = decodeURIComponent(authorId || '');
|
|
20
|
+
var listEl = document.getElementById('list');
|
|
21
|
+
var subEl = document.getElementById('subtitle');
|
|
22
|
+
|
|
23
|
+
if (!authorId) {
|
|
24
|
+
subEl.textContent = '';
|
|
25
|
+
listEl.innerHTML = '<div class="error">缺少博主 ID(正确形式:blogger.html#id=<author_id>)</div>';
|
|
26
|
+
} else {
|
|
27
|
+
BLOG.init().then(function () {
|
|
28
|
+
if (BLOG.isSharedView()) {
|
|
29
|
+
document.getElementById('banner').innerHTML =
|
|
30
|
+
'<div class="banner">当前为分享只读视图:仅可浏览该博主已发布文章。</div>';
|
|
31
|
+
}
|
|
32
|
+
return BLOG.loadPostsByAuthor(authorId);
|
|
33
|
+
}).then(function (rows) {
|
|
34
|
+
// 只展示已发布文章(游客视角);登录博主看自己的草稿仍走管理页。
|
|
35
|
+
var published = (rows || []).filter(function (p) { return p.status === 'published'; });
|
|
36
|
+
var name = (published[0] && published[0].author_name) || (rows && rows[0] && rows[0].author_name) || '匿名博主';
|
|
37
|
+
document.getElementById('title').textContent = '👤 ' + name;
|
|
38
|
+
subEl.textContent = published.length ? ('共 ' + published.length + ' 篇已发布文章(新→旧)') : '该博主暂无已发布文章。';
|
|
39
|
+
if (!published.length) { listEl.innerHTML = ''; return; }
|
|
40
|
+
listEl.innerHTML = published.map(function (p) {
|
|
41
|
+
return '<div class="card"><div class="row"><h2><a href="./article.html#id=' + encodeURIComponent(p.id) + '">' +
|
|
42
|
+
BLOG.esc(p.title) + '</a></h2></div>' +
|
|
43
|
+
'<p class="content">' + BLOG.esc(String(p.content || '').slice(0, 140)) + '</p>' +
|
|
44
|
+
'<div class="muted">#' + BLOG.esc(p.id) + ' · ' + BLOG.esc(BLOG.fmtDate(p.created_at)) + '</div></div>';
|
|
45
|
+
}).join('');
|
|
46
|
+
}).catch(function (e) {
|
|
47
|
+
subEl.textContent = '';
|
|
48
|
+
listEl.innerHTML = '<div class="error">加载失败: ' + BLOG.esc(e) + '</div>';
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>博客广场 — MPA Blog</title>
|
|
7
|
+
<link rel="stylesheet" href="./style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<h1>📚 博客广场</h1>
|
|
11
|
+
<nav>
|
|
12
|
+
<a href="./index.html">广场</a>
|
|
13
|
+
<a href="./admin.html" id="nav-admin" style="display:none">博主管理</a>
|
|
14
|
+
<a href="./profile.html" id="nav-profile" style="display:none">我的资料</a>
|
|
15
|
+
<span class="spacer"></span>
|
|
16
|
+
<span id="authbox" class="muted"></span>
|
|
17
|
+
</nav>
|
|
18
|
+
<div id="banner"></div>
|
|
19
|
+
<div class="muted" style="margin-bottom:8px">按博主展示:每位博主的最新 1~2 篇文章;点「更多文章」进入该博主主页(只看他/她的文章)。游客仅可浏览。</div>
|
|
20
|
+
<div id="list"><div class="muted">加载中…</div></div>
|
|
21
|
+
<script src="./blog.js"></script>
|
|
22
|
+
<script>
|
|
23
|
+
BLOG.init().then(function () {
|
|
24
|
+
if (BLOG.isSharedView()) {
|
|
25
|
+
document.getElementById('banner').innerHTML =
|
|
26
|
+
'<div class="banner">当前为分享只读视图:可浏览已发布文章;登录与管理请走函数原始 URL。</div>';
|
|
27
|
+
}
|
|
28
|
+
return BLOG.me();
|
|
29
|
+
}).then(function (m) {
|
|
30
|
+
var box = document.getElementById('authbox');
|
|
31
|
+
if (m.user) {
|
|
32
|
+
document.getElementById('nav-admin').style.display = '';
|
|
33
|
+
document.getElementById('nav-profile').style.display = '';
|
|
34
|
+
box.innerHTML = BLOG.esc(m.user.email) + ' · <a href="#" id="logout">退出</a>';
|
|
35
|
+
document.getElementById('logout').onclick = function (e) {
|
|
36
|
+
e.preventDefault(); BLOG.clearToken(); BLOG.setMeta({}); location.href = './index.html';
|
|
37
|
+
};
|
|
38
|
+
} else if (!BLOG.isSharedView()) {
|
|
39
|
+
box.innerHTML = '<a href="./login.html">登录</a> · <a href="./signup.html">注册</a>';
|
|
40
|
+
} else {
|
|
41
|
+
// 只读分享视图:仍给出相对路径的登录/注册入口(在 mount 前缀下同样有效)。
|
|
42
|
+
box.innerHTML = '<span class="muted">游客</span> · <a href="./login.html">登录</a> · <a href="./signup.html">注册</a>';
|
|
43
|
+
}
|
|
44
|
+
return BLOG.loadPosts();
|
|
45
|
+
}).then(function (rows) {
|
|
46
|
+
var el = document.getElementById('list');
|
|
47
|
+
// 广场为纯公开视图:只展示已发布文章(草稿仅作者本人在管理页可见)。
|
|
48
|
+
var published = (rows || []).filter(function (p) { return p.status === 'published'; });
|
|
49
|
+
var groups = BLOG.groupByAuthor(published);
|
|
50
|
+
if (!groups.length) { el.innerHTML = '<div class="muted">广场暂无已发布文章。</div>'; return; }
|
|
51
|
+
el.innerHTML = groups.map(function (g) {
|
|
52
|
+
var name = g.author_name || '匿名博主';
|
|
53
|
+
var latest = g.posts.slice(0, 2).map(function (p) {
|
|
54
|
+
return '<li><a href="./article.html#id=' + encodeURIComponent(p.id) + '">' + BLOG.esc(p.title) + '</a>' +
|
|
55
|
+
' <span class="muted">' + BLOG.esc(BLOG.fmtDate(p.created_at)) + '</span></li>';
|
|
56
|
+
}).join('');
|
|
57
|
+
var more = g.count > 2 ? '<span class="muted">共 ' + g.count + ' 篇(只显示最新 2 篇)</span>' : '<span class="muted">' + g.count + ' 篇</span>';
|
|
58
|
+
return '<div class="card blogger">' +
|
|
59
|
+
'<div class="row"><h2>👤 ' + BLOG.esc(name) + '</h2>' +
|
|
60
|
+
'<a class="more" href="./blogger.html#id=' + encodeURIComponent(g.author_id) + '">更多文章 →</a></div>' +
|
|
61
|
+
'<div class="muted" style="margin:2px 0 8px">' + more + '</div>' +
|
|
62
|
+
'<ul class="postlist">' + latest + '</ul>' +
|
|
63
|
+
'</div>';
|
|
64
|
+
}).join('');
|
|
65
|
+
}).catch(function (e) {
|
|
66
|
+
document.getElementById('list').innerHTML = '<div class="error">加载失败: ' + BLOG.esc(e) + '</div>';
|
|
67
|
+
});
|
|
68
|
+
</script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>MPA Blog — 博主登录</title>
|
|
7
|
+
<link rel="stylesheet" href="./style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<nav><a href="./index.html">← 返回广场</a></nav>
|
|
11
|
+
<h1>博主登录</h1>
|
|
12
|
+
<div class="card">
|
|
13
|
+
<input id="email" type="email" placeholder="邮箱" autocomplete="username">
|
|
14
|
+
<input id="password" type="password" placeholder="密码" autocomplete="current-password">
|
|
15
|
+
<div class="row">
|
|
16
|
+
<button id="login">登录</button>
|
|
17
|
+
<span class="muted">还没有账号?<a href="./signup.html">注册新博主</a></span>
|
|
18
|
+
</div>
|
|
19
|
+
<div id="msg" class="muted"></div>
|
|
20
|
+
</div>
|
|
21
|
+
<script src="./blog.js"></script>
|
|
22
|
+
<script>
|
|
23
|
+
var msg = document.getElementById('msg');
|
|
24
|
+
var btn = document.getElementById('login');
|
|
25
|
+
var emailEl = document.getElementById('email');
|
|
26
|
+
var pwEl = document.getElementById('password');
|
|
27
|
+
|
|
28
|
+
function doLogin() {
|
|
29
|
+
if (btn.disabled) return;
|
|
30
|
+
var email = emailEl.value.trim();
|
|
31
|
+
if (!email || !pwEl.value) { msg.className = 'error'; msg.textContent = '请填写邮箱和密码'; return; }
|
|
32
|
+
msg.className = 'muted'; msg.textContent = '登录中…';
|
|
33
|
+
btn.disabled = true;
|
|
34
|
+
BLOG.login(email, pwEl.value).then(function (r) {
|
|
35
|
+
return r.json().catch(function () { return {}; }).then(function (j) {
|
|
36
|
+
if (!r.ok || !j.access_token) {
|
|
37
|
+
msg.className = 'error'; msg.textContent = BLOG.authError(j, r.status); btn.disabled = false; return;
|
|
38
|
+
}
|
|
39
|
+
BLOG.setToken(j.access_token);
|
|
40
|
+
BLOG.setMeta((j.user && j.user.user_metadata) || {});
|
|
41
|
+
location.href = './admin.html';
|
|
42
|
+
});
|
|
43
|
+
}).catch(function (e) { msg.className = 'error'; msg.textContent = String(e); btn.disabled = false; });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
BLOG.init().then(function () { return BLOG.me(); }).then(function (m) {
|
|
47
|
+
if (m && m.user) { location.replace('./admin.html'); return; }
|
|
48
|
+
btn.onclick = doLogin;
|
|
49
|
+
[emailEl, pwEl].forEach(function (el) {
|
|
50
|
+
el.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') { ev.preventDefault(); doLogin(); } });
|
|
51
|
+
});
|
|
52
|
+
emailEl.focus();
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>MPA Blog — 我的资料</title>
|
|
7
|
+
<link rel="stylesheet" href="./style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<nav><a href="./index.html">← 返回广场</a><a href="./admin.html">博主管理</a></nav>
|
|
11
|
+
<div id="gate"></div>
|
|
12
|
+
<div id="box" style="display:none">
|
|
13
|
+
<h1>我的资料</h1>
|
|
14
|
+
<div class="card">
|
|
15
|
+
<div class="muted">邮箱</div>
|
|
16
|
+
<div id="email" style="font-size:16px;margin:4px 0 12px"></div>
|
|
17
|
+
<div class="muted">注册时间</div>
|
|
18
|
+
<div id="created" style="margin:4px 0 12px"></div>
|
|
19
|
+
<div class="muted">展示名(发文署名)</div>
|
|
20
|
+
<input id="displayName" placeholder="展示名,留空则用邮箱前缀" maxlength="40">
|
|
21
|
+
<button id="save">保存资料</button>
|
|
22
|
+
<div id="msg" class="muted"></div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<script src="./blog.js"></script>
|
|
26
|
+
<script>
|
|
27
|
+
var gate = document.getElementById('gate');
|
|
28
|
+
BLOG.init().then(function () {
|
|
29
|
+
if (BLOG.isSharedView()) {
|
|
30
|
+
gate.innerHTML = '<div class="banner">分享只读视图:资料管理已禁用。请通过函数原始 URL 打开本页。</div>';
|
|
31
|
+
return Promise.reject(new Error('shared-view'));
|
|
32
|
+
}
|
|
33
|
+
return BLOG.me().then(function (m) {
|
|
34
|
+
if (!m.user) { location.replace('./login.html'); return Promise.reject(new Error('anon')); }
|
|
35
|
+
return BLOG.getProfile().then(function (u) {
|
|
36
|
+
document.getElementById('box').style.display = '';
|
|
37
|
+
document.getElementById('email').textContent = u.email || m.user.email || '';
|
|
38
|
+
document.getElementById('created').textContent = BLOG.fmtDate(u.created_at);
|
|
39
|
+
var meta = u.user_metadata || {};
|
|
40
|
+
document.getElementById('displayName').value = meta.display_name || '';
|
|
41
|
+
BLOG.setMeta(meta);
|
|
42
|
+
document.getElementById('save').onclick = function () {
|
|
43
|
+
var msg = document.getElementById('msg');
|
|
44
|
+
var name = document.getElementById('displayName').value.trim();
|
|
45
|
+
BLOG.updateProfile({ display_name: name }).then(function (u2) {
|
|
46
|
+
BLOG.setMeta(u2.user_metadata || { display_name: name });
|
|
47
|
+
msg.className = 'muted'; msg.textContent = '已保存;新发文章将使用该署名。';
|
|
48
|
+
}).catch(function (e) { msg.className = 'error'; msg.textContent = String(e); });
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}).catch(function (e) {
|
|
53
|
+
if (String(e) !== 'Error: shared-view' && String(e) !== 'Error: anon') {
|
|
54
|
+
gate.innerHTML = '<div class="error">加载失败: ' + BLOG.esc(e) + '</div>';
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
</script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|