ChatComment 0.1.0__tar.gz → 0.1.1__tar.gz

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.
Files changed (27) hide show
  1. {chatcomment-0.1.0 → chatcomment-0.1.1}/CHANGELOG.md +5 -0
  2. {chatcomment-0.1.0 → chatcomment-0.1.1}/PKG-INFO +4 -2
  3. {chatcomment-0.1.0 → chatcomment-0.1.1}/README.md +3 -1
  4. {chatcomment-0.1.0 → chatcomment-0.1.1}/docs/api.md +4 -0
  5. {chatcomment-0.1.0 → chatcomment-0.1.1}/docs/deployment.md +2 -0
  6. {chatcomment-0.1.0 → chatcomment-0.1.1}/pyproject.toml +1 -1
  7. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/ChatComment.egg-info/PKG-INFO +4 -2
  8. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/ChatComment.egg-info/SOURCES.txt +1 -0
  9. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/chatcomment/__init__.py +1 -1
  10. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/chatcomment/server.py +11 -1
  11. chatcomment-0.1.1/src/chatcomment/static/login.html +17 -0
  12. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/chatcomment/static/widget.js +17 -3
  13. {chatcomment-0.1.0 → chatcomment-0.1.1}/tests/code-tests/test_comments.py +4 -0
  14. {chatcomment-0.1.0 → chatcomment-0.1.1}/AGENTS.md +0 -0
  15. {chatcomment-0.1.0 → chatcomment-0.1.1}/LICENSE +0 -0
  16. {chatcomment-0.1.0 → chatcomment-0.1.1}/MANIFEST.in +0 -0
  17. {chatcomment-0.1.0 → chatcomment-0.1.1}/setup.cfg +0 -0
  18. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/ChatComment.egg-info/dependency_links.txt +0 -0
  19. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/ChatComment.egg-info/entry_points.txt +0 -0
  20. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/ChatComment.egg-info/requires.txt +0 -0
  21. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/ChatComment.egg-info/top_level.txt +0 -0
  22. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/chatcomment/cli.py +0 -0
  23. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/chatcomment/static/admin.html +0 -0
  24. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/chatcomment/static/board.html +0 -0
  25. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/chatcomment/static/widget.css +0 -0
  26. {chatcomment-0.1.0 → chatcomment-0.1.1}/src/chatcomment/store.py +0 -0
  27. {chatcomment-0.1.0 → chatcomment-0.1.1}/tests/cli-tests/test_cli.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1
4
+
5
+ - 留言区仅显示一个齿轮入口;本人填写管理员邮箱后由服务器确认显示,不按昵称判断、不下发管理员邮箱。
6
+ - 提供邮箱/密码验证页,网关可在现有账号校验后授予短时管理权限;管理 API 仍由服务器角色授权。
7
+
3
8
  ## 0.1.0
4
9
 
5
10
  - Python/SQLite 留言服务、无依赖 JS 组件及同源管理界面。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ChatComment
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Embeddable comments with a Python server, SQLite storage and a small JavaScript widget.
5
5
  Author: ChatArch
6
6
  License-Expression: MIT
@@ -43,4 +43,6 @@ chatcomment serve --token-file ~/.chatarch/chatcomment/admin-token
43
43
 
44
44
  API、网关接入、备份与恢复见 [docs/api.md](docs/api.md)、[docs/deployment.md](docs/deployment.md)。本包不自动开放防火墙或注册 DNS,也不包含云服务凭据。
45
45
 
46
- 独立留言板为 `/board?site=personal-blog&page=%2Fpost%2Fone`,与嵌入表单读写同一留言流。设置按钮只在服务器返回 `can_manage=true` 时显示。网关为普通成员设置 `X-ChatComment-Role: reader`,仅经服务端验证的 owner 使用 `admin`;管理接口另行验证权限。公开浏览器不能靠昵称或伪造角色 header 获得权限,因为缺少服务器 Bearer token。
46
+ 独立留言板为 `/board?site=personal-blog&page=%2Fpost%2Fone`,与嵌入表单读写同一留言流。组件只提供一个齿轮入口:已登录 owner 在邮箱栏填写匹配邮箱后,服务器返回显示提示;昵称不参与判断,邮箱不下发到 JS 配置。齿轮指向邮箱/密码验证页;网关须实现 `/api/admin/reauth` 并在校验现有账号后授予短时管理权限。公开站点没有该提示。
47
+
48
+ 网关为普通成员设置 `X-ChatComment-Role: reader`,仅经服务端验证的 owner 使用 `admin`;管理接口另行验证权限。公开浏览器不能靠昵称、邮箱或伪造角色 header 获得权限,因为缺少服务器 Bearer token。服务器可为受信 owner 请求附加 `X-ChatComment-Admin-Email`,只用于入口提示。完整验证合约见部署文档。
@@ -24,4 +24,6 @@ chatcomment serve --token-file ~/.chatarch/chatcomment/admin-token
24
24
 
25
25
  API、网关接入、备份与恢复见 [docs/api.md](docs/api.md)、[docs/deployment.md](docs/deployment.md)。本包不自动开放防火墙或注册 DNS,也不包含云服务凭据。
26
26
 
27
- 独立留言板为 `/board?site=personal-blog&page=%2Fpost%2Fone`,与嵌入表单读写同一留言流。设置按钮只在服务器返回 `can_manage=true` 时显示。网关为普通成员设置 `X-ChatComment-Role: reader`,仅经服务端验证的 owner 使用 `admin`;管理接口另行验证权限。公开浏览器不能靠昵称或伪造角色 header 获得权限,因为缺少服务器 Bearer token。
27
+ 独立留言板为 `/board?site=personal-blog&page=%2Fpost%2Fone`,与嵌入表单读写同一留言流。组件只提供一个齿轮入口:已登录 owner 在邮箱栏填写匹配邮箱后,服务器返回显示提示;昵称不参与判断,邮箱不下发到 JS 配置。齿轮指向邮箱/密码验证页;网关须实现 `/api/admin/reauth` 并在校验现有账号后授予短时管理权限。公开站点没有该提示。
28
+
29
+ 网关为普通成员设置 `X-ChatComment-Role: reader`,仅经服务端验证的 owner 使用 `admin`;管理接口另行验证权限。公开浏览器不能靠昵称、邮箱或伪造角色 header 获得权限,因为缺少服务器 Bearer token。服务器可为受信 owner 请求附加 `X-ChatComment-Admin-Email`,只用于入口提示。完整验证合约见部署文档。
@@ -12,6 +12,8 @@ All request bodies are JSON (maximum 32 KiB). Site IDs are explicit, origins are
12
12
  | GET | `/api/sites/{id}/comments?page=/path/` | Approved comments, never emails |
13
13
  | POST | `/api/sites/{id}/comments` | `{page,content,nickname?,email?,website?}` |
14
14
  | GET | `/admin` | Gateway-protected management UI |
15
+ | GET | `/login` | Owner reauthentication form; gateway owns password verification |
16
+ | POST | `/api/admin/entry` | `{email}` → `{show_settings}`; trusted owner only, hint never authorizes management |
15
17
  | GET | `/board?site={id}&page=/path/` | Standalone widget host; apply the private-site gateway to this HTML too |
16
18
  | GET/POST | `/api/admin/sites` | List/register sites |
17
19
  | POST | `/api/sites/{id}/settings` | Complete settings object |
@@ -25,6 +27,8 @@ Private sites and all admin calls require `Authorization: Bearer <server token>`
25
27
 
26
28
  Gateway members use `X-ChatComment-Role: reader`; they can read a private site and submit with a verified identity but cannot administer. Only owner requests receive `admin`. A direct bearer API client defaults to admin for compatibility. `/config` returns `can_manage` from this trusted role, and the widget shows the settings link accordingly. Private HTML, site read permissions and site-specific authorization remain the gateway's responsibility.
27
29
 
30
+ In 0.1.1 the widget additionally requires a successful `/api/admin/entry` hint for the entered email before displaying its single gear. The expected email comes only from `X-ChatComment-Admin-Email` on a trusted server request. Nicknames have no role. The `/login` form posts `{email,password}` to gateway-owned `/api/admin/reauth` with native session CSRF. The gateway must validate the existing account password and owner role, issue its native cookie, and enforce a short-lived management grant on `/admin` and all management API routes. A hint response alone must never grant that access.
31
+
28
32
  Settings example:
29
33
 
30
34
  ```json
@@ -6,6 +6,8 @@ For a private site, apply the existing site login to **every** API and asset req
6
6
 
7
7
  The gateway maps its prefix to backend `/`, injects the Bearer token and a verified identity marker, and relays only required response headers. Block unexpected methods and cap body size. A separate public site may explicitly use `site register --public`, but the private site setting must remain private. Site IDs are not secrets and never replace access control.
8
8
 
9
+ For the single-gear workflow, inject the configured owner email only as the server-side `X-ChatComment-Admin-Email` header. Implement `/api/admin/reauth` at the gateway: check current session and CSRF, verify the supplied email/password through the existing account backend, verify its owner role, rotate the native session cookie and create a short-lived grant bound to that session. Protect `/admin`, `/api/admin/sites` and site settings/admin/export/moderate routes with this grant. Reject readers even if they type the owner email or forge role headers. Keep email hints, private read access and management grants distinct. The included login HTML stores no credentials and delegates password verification entirely to this gateway contract.
10
+
9
11
  Consistent live backup:
10
12
 
11
13
  ```sh
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ChatComment"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Embeddable comments with a Python server, SQLite storage and a small JavaScript widget."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ChatComment
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Embeddable comments with a Python server, SQLite storage and a small JavaScript widget.
5
5
  Author: ChatArch
6
6
  License-Expression: MIT
@@ -43,4 +43,6 @@ chatcomment serve --token-file ~/.chatarch/chatcomment/admin-token
43
43
 
44
44
  API、网关接入、备份与恢复见 [docs/api.md](docs/api.md)、[docs/deployment.md](docs/deployment.md)。本包不自动开放防火墙或注册 DNS,也不包含云服务凭据。
45
45
 
46
- 独立留言板为 `/board?site=personal-blog&page=%2Fpost%2Fone`,与嵌入表单读写同一留言流。设置按钮只在服务器返回 `can_manage=true` 时显示。网关为普通成员设置 `X-ChatComment-Role: reader`,仅经服务端验证的 owner 使用 `admin`;管理接口另行验证权限。公开浏览器不能靠昵称或伪造角色 header 获得权限,因为缺少服务器 Bearer token。
46
+ 独立留言板为 `/board?site=personal-blog&page=%2Fpost%2Fone`,与嵌入表单读写同一留言流。组件只提供一个齿轮入口:已登录 owner 在邮箱栏填写匹配邮箱后,服务器返回显示提示;昵称不参与判断,邮箱不下发到 JS 配置。齿轮指向邮箱/密码验证页;网关须实现 `/api/admin/reauth` 并在校验现有账号后授予短时管理权限。公开站点没有该提示。
47
+
48
+ 网关为普通成员设置 `X-ChatComment-Role: reader`,仅经服务端验证的 owner 使用 `admin`;管理接口另行验证权限。公开浏览器不能靠昵称、邮箱或伪造角色 header 获得权限,因为缺少服务器 Bearer token。服务器可为受信 owner 请求附加 `X-ChatComment-Admin-Email`,只用于入口提示。完整验证合约见部署文档。
@@ -18,6 +18,7 @@ src/chatcomment/server.py
18
18
  src/chatcomment/store.py
19
19
  src/chatcomment/static/admin.html
20
20
  src/chatcomment/static/board.html
21
+ src/chatcomment/static/login.html
21
22
  src/chatcomment/static/widget.css
22
23
  src/chatcomment/static/widget.js
23
24
  tests/cli-tests/test_cli.py
@@ -1,5 +1,5 @@
1
1
  """Reusable comments storage and HTTP service."""
2
- __version__ = "0.1.0"
2
+ __version__ = "0.1.1"
3
3
 
4
4
  from .store import Store
5
5
 
@@ -11,7 +11,7 @@ import time
11
11
  from urllib.parse import parse_qs, urlsplit
12
12
 
13
13
  from . import __version__
14
- from .store import Invalid, Store, origin
14
+ from .store import Invalid, Store, origin, text
15
15
 
16
16
 
17
17
  def make_handler(store: Store, admin_token: str):
@@ -83,6 +83,16 @@ def make_handler(store: Store, admin_token: str):
83
83
  if not manager:
84
84
  raise PermissionError('请通过已登录管理网关访问')
85
85
  return self.send(200, files('chatcomment').joinpath('static/admin.html').read_bytes(), 'text/html; charset=utf-8')
86
+ if p.path == '/login' and self.command in {'GET','HEAD'}:
87
+ if not manager:
88
+ raise PermissionError('请通过已登录管理网关访问')
89
+ return self.send(200, files('chatcomment').joinpath('static/login.html').read_bytes(), 'text/html; charset=utf-8')
90
+ if p.path == '/api/admin/entry' and self.command == 'POST':
91
+ if not manager:
92
+ raise PermissionError('需要本人账号')
93
+ entered = text(self.payload().get('email',''),'邮箱',500)
94
+ expected = self.headers.get('X-ChatComment-Admin-Email','')
95
+ return self.send(200, {'show_settings':bool(expected) and secrets.compare_digest(entered.casefold().encode(),expected.strip().casefold().encode())})
86
96
  if p.path == '/api/admin/sites':
87
97
  if not manager:
88
98
  raise PermissionError('需要管理权限')
@@ -0,0 +1,17 @@
1
+ <!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ChatComment · 验证管理身份</title>
2
+ <style>body{margin:0;background:#f0f4f0;font:16px/1.6 system-ui,sans-serif;color:#243d30}main{max-width:420px;margin:10vh auto;padding:28px;background:white;border:1px solid #d7e0d9;border-radius:14px}label{display:block;margin:18px 0}input{display:block;box-sizing:border-box;width:100%;font:inherit;padding:10px;border:1px solid #c7d4ca;border-radius:7px}button{font:inherit;background:#2c6550;color:white;border:0;border-radius:7px;padding:10px 18px;cursor:pointer}a{color:#2d6953}p{font-size:14px;color:#617366}</style>
3
+ <main><a id="back">← 返回页面</a><h1>验证管理身份</h1><p>使用既有站点账号的邮箱和密码。只有本人账号可管理此留言区。</p><form><label>邮箱<input name="email" type="email" autocomplete="username" required></label><label>密码<input name="password" type="password" autocomplete="current-password" required></label><button>验证并进入设置</button><p role="status" aria-live="polite"></p></form></main>
4
+ <script>
5
+ (() => {
6
+ const base=location.pathname.replace(/\/login\/?$/,'');
7
+ document.getElementById('back').href=base.replace(/\/comments$/,'/')||'/';
8
+ const form=document.querySelector('form'),status=document.querySelector('[role=status]');
9
+ form.addEventListener('submit',async e=>{e.preventDefault();const button=form.querySelector('button');button.disabled=true;status.textContent='正在验证…';
10
+ try{const session=await fetch('/api/session',{credentials:'same-origin'});if(!session.ok)throw new Error('请先登录站点');const csrf=(await session.json()).csrf_token;
11
+ const response=await fetch(base+'/api/admin/reauth',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify(Object.fromEntries(new FormData(form)))});
12
+ const result=await response.json();if(!response.ok)throw new Error(result.error||'邮箱或密码不正确');
13
+ form.elements.password.value='';location.assign(base+'/admin');
14
+ }catch(error){status.textContent=error.message;form.elements.password.value='';button.disabled=false;}
15
+ });
16
+ })();
17
+ </script></html>
@@ -12,13 +12,13 @@
12
12
  const link = document.createElement('link');
13
13
  link.rel = 'stylesheet'; link.href = `${api}/widget.css`; root.append(link);
14
14
  const box = document.createElement('section'); box.className = 'chatcomment'; root.append(box);
15
- box.innerHTML = `<header><div><p class="cc-eyebrow">ChatComment</p><h2>留下想法</h2></div><a class="cc-admin" hidden>设置与管理</a></header>
15
+ box.innerHTML = `<header><div><p class="cc-eyebrow">ChatComment</p><h2>留下想法</h2></div><a class="cc-admin" aria-label="留言设置" title="留言设置" hidden>⚙</a></header>
16
16
  <p class="cc-note">留言保存在服务器,刷新后仍可查看。邮箱仅管理员可见。</p>
17
17
  <form><div class="cc-fields"></div><label>留言 <span>必填</span><textarea name="content" required maxlength="5000" rows="4" placeholder="有什么想法,或者值得接着做的事?"></textarea></label>
18
18
  <div class="cc-actions"><button type="submit">提交留言</button><button type="button" class="cc-refresh">刷新留言</button><span class="cc-status" role="status" aria-live="polite"></span></div></form>
19
19
  <div class="cc-list" aria-live="polite"></div><button type="button" class="cc-more" hidden>更早的留言</button>`;
20
20
  // The API base comes from the deployer's script attribute; never from comments.
21
- box.querySelector('.cc-admin').href = `${api}/admin`;
21
+ box.querySelector('.cc-admin').href = `${api}/login`;
22
22
  const status = box.querySelector('.cc-status');
23
23
  const list = box.querySelector('.cc-list');
24
24
  const form = box.querySelector('form');
@@ -43,7 +43,7 @@
43
43
  }
44
44
  async function config() {
45
45
  const data = await request(`/api/sites/${site}/config`);
46
- box.querySelector('.cc-admin').hidden = data.can_manage !== true;
46
+ const gear = box.querySelector('.cc-admin'); gear.hidden = true;
47
47
  const fields = box.querySelector('.cc-fields'); fields.replaceChildren();
48
48
  const labels = {nickname:'昵称',email:'邮箱',website:'网址'};
49
49
  for (const [name, mode] of Object.entries(data.settings.fields)) {
@@ -55,6 +55,20 @@
55
55
  input.type = name === 'email' ? 'email' : name === 'website' ? 'url' : 'text';
56
56
  input.maxLength = name === 'nickname' ? 120 : 500;
57
57
  label.append(input); fields.append(label);
58
+ if (name === 'email' && data.can_manage === true) {
59
+ let timer;
60
+ input.addEventListener('input', () => {
61
+ clearTimeout(timer); gear.hidden = true;
62
+ const value = input.value.trim();
63
+ if (!value || !input.validity.valid) return;
64
+ timer = setTimeout(async () => {
65
+ try {
66
+ const hint = await request('/api/admin/entry', {email:value});
67
+ gear.hidden = input.value.trim() !== value || hint.show_settings !== true;
68
+ } catch (_) { gear.hidden = true; }
69
+ }, 250);
70
+ });
71
+ }
58
72
  }
59
73
  box.querySelector('.cc-note').textContent = `${data.settings.allow_anonymous?'无需留言账号,可匿名提交。':'需要已验证的站点身份。'}${data.settings.moderation?'留言经审核后显示。':''}邮箱仅管理员可见。`;
60
74
  }
@@ -97,6 +97,10 @@ def test_http_private_admin_origins_and_no_header_spoof(http):
97
97
  assert http('/api/sites/second/config',trusted=True,extra={'X-ChatComment-Role':'reader'})[2]['can_manage'] is False
98
98
  assert http('/api/sites/first/config')[2]['can_manage'] is False
99
99
  assert http('/api/sites/first/config',trusted=True)[2]['can_manage'] is True
100
+ assert http('/api/admin/entry',{'email':'rexwzh@example.invalid'})[0]==403
101
+ assert http('/api/admin/entry',{'email':'rexwzh@example.invalid'},trusted=True,extra={'X-ChatComment-Role':'reader'})[0]==403
102
+ assert http('/api/admin/entry',{'email':'other@example.invalid'},trusted=True,extra={'X-ChatComment-Admin-Email':'rexwzh@example.invalid'})[2]['show_settings'] is False
103
+ assert http('/api/admin/entry',{'email':'RexWzh@example.invalid'},trusted=True,extra={'X-ChatComment-Admin-Email':'rexwzh@example.invalid'})[2]['show_settings'] is True
100
104
  assert http('/api/sites/second/settings',{},trusted=True,extra={'X-ChatComment-Role':'reader'})[0]==403
101
105
  assert http('/api/admin/sites')[0]==403
102
106
  assert http('/api/sites/second/config',identity=True)[0]==403
File without changes
File without changes
File without changes
File without changes