资源附件+共享:补 /resources/<id>/{attach,shares} API 端点 #25

用户原话(2026-10-28):「鸿蒙端对齐网页端」。资源端上明说「附件与共享
还在网页端」,根因是资源 API 只有 title/content,附件与共享没接口。

补两组子端点(模型 Resource::ATTACH=resource、shares/setShare/dropShare 早就有):
· GET/POST/DELETE /api/v1/resources/<id>/attach —— 附件列表 / 传 / 删
  (删走 attachment_id query,与 tasks 同招;可写者才允许,别人家的 404)
· GET/POST/DELETE /api/v1/resources/<id>/shares —— 共享列表 / 加 / 取消
  (canWrite 才允许,越权 404 —— find 对看不到的资源返 null,不泄露存在性)

判据:_i25_resource_api.py 21/0(传/列/删附件 · 共享增/列/删 · 越权被拒且库里没变)
· _i25_falsify.py 注掉 attach 列表与 shares 分支各自变红。
This commit is contained in:
2026-10-28 22:00:14 +08:00
parent e44f37e902
commit 47247e39b9
+69 -2
View File
@@ -109,7 +109,7 @@ final class ApiHandler
case 'vehicles': $this->vehicles($id, $method); return;
case 'nodetypes': $this->nodeTypes($id, $method); return;
case 'ledger': $this->ledger($id, $sub, $method); return;
case 'resources': $this->resources($id, $method); return;
case 'resources': $this->resources($id, $sub, $method); return;
case 'books': $this->books($id, $sub, $method); return;
case 'notes': $this->notes($id, $sub, $method); return;
// 🔴 v45:登录(手机号 + 密码 → API 令牌)。
@@ -2623,12 +2623,79 @@ final class ApiHandler
资源 / 书 / 笔记
═══════════════════════════════════════════════════════════════ */
private function resources(int $id, string $method): void
private function resources(int $id, string $sub, string $method): void
{
[$fam, $pid] = Api::me();
if ($id > 0) {
$r = Resource::find($fam, $id, $pid);
if (!$r) { Api::notFound('这条资源'); }
/* 🔴 工单 #25(用户 2026-10-28「鸿蒙端对齐网页端」):资源附件 + 共享。
端上原来明说「附件与共享还在网页端」,服务端这里补 API。
⚠️ 三段路由(`Api::dispatch` 只解析 /resources/<id>/<sub>),
删附件用 `attachment_id` 参数(与 tasks 同招),不是第 4 段。 */
if ($sub === 'attach') {
if ($method === 'GET') {
Api::ok(['items' => \App\Attachment::list(Resource::ATTACH, $id)]);
}
Api::requireWrite();
if (!Resource::canWrite($r, $pid)) {
Api::fail('这条资源不能改。', 'forbidden', 403);
}
if ($method === 'POST') {
$file = $_FILES['file'] ?? null;
if (!is_array($file)) {
Api::fail('没有收到文件。', 'unprocessable', 422);
}
$res = \App\Attachment::save($fam, Resource::ATTACH, $id, $file, $pid);
if (empty($res['ok'])) {
$m = (string)($res['error'] ?? '');
Api::fail($m !== '' ? $m : '文件没存下来', 'unprocessable', 422);
}
Api::ok(['id' => (int)($res['id'] ?? 0)], 201);
}
if ($method === 'DELETE') {
$aid = Api::int('attachment_id');
if ($aid <= 0) {
Api::fail('要指定删哪个附件。', 'bad_request', 400);
}
if (!\App\Attachment::removeFrom(Resource::ATTACH, $id, $aid)) {
Api::notFound('这个附件');
}
Api::ok(['deleted' => $aid]);
}
Api::fail('不支持的方法。', 'method_not_allowed', 405);
}
if ($sub === 'shares') {
$isOwner = (int)($r['owner_id'] ?? 0) === $pid;
if ($method === 'GET') {
Api::ok(['items' => Resource::shares($id), 'is_owner' => $isOwner]);
}
// 只有 owner 或可写的人能改共享(Web 端 resources/share 的 canWrite 语义)
Api::requireWrite();
if (!Resource::canWrite($r, $pid)) {
Api::fail('这条资源不能共享。', 'forbidden', 403);
}
if ($method === 'POST') {
$who = Api::int('person_id');
if ($who <= 0) {
Api::fail('要共享给谁?缺 `person_id`。', 'unprocessable', 422);
}
Resource::setShare($id, (int)$r['owner_id'], $who, Api::str('perm', 'read'));
Api::ok(['items' => Resource::shares($id)]);
}
if ($method === 'DELETE') {
$who = Api::int('person_id');
if ($who <= 0) {
Api::fail('要取消谁的共享?缺 `person_id`。', 'unprocessable', 422);
}
Resource::dropShare($id, $who);
Api::ok(['items' => Resource::shares($id)]);
}
Api::fail('不支持的方法。', 'method_not_allowed', 405);
}
if ($method === 'GET') { Api::ok($r); }
if ($method === 'PATCH' || $method === 'PUT') {
Api::requireWrite();