v44 第 44 批:界面 5 条 + 移动端 API v1 全量

界面(用户 2026-10-12 第 1~5 条)
1. 新建日程「日历本」并入 .cs 自绘下拉,颜色画进候选项,三字与下拉同行。
2. 新建日程「开始/结束」改「日期范围:」,删旧日期控件;
   重复相关三行仅选重复时出现;删「(含第一次)」与「这段时间算忙碌」。
3. 条目浮层加「地点」、去掉「状态」行;首页红线按分钟定位(原只取整小时);
   去掉左侧红点只留红线。
4. 任务页删掉状态说明提示(STATUS_HINT 三值清空)。
5. 新建任务:标题在前树框在后;截止时间改最新日历控件;负责人默认自己;
   删冗余提示;「上面要做什么」→「任务名称」;清理括号说明。

移动端 API(用户第 6 条:导航模块除外,其余全量)
- 新增 Bearer 令牌鉴权(api_tokens 表,Schema v32),与登录口令 / DAV 口令三套分立。
- Api.php 内核(鉴权/取参/信封/分页/分发 + _method 覆写)+
  ApiHandler.php 17 条资源 + ApiToken.php + api.php 入口。
- 路由:router.php / .htaccess / index.php 三处 /api/ 兜底(部署脚本自动追加线上规则)。
- 个人设置新增「App 接口令牌」卡;docs/API.md 接口文档;
  docs/API-QUESTION-LIST.md 自主决策与待确认缺口清单。

验证:本地 e2e 1510/0、v43 真机 33/0、v43 反向 9/0、
v44 真机 41/0、v44 API 63/0、v44 反向 19/0;全流水线 PUSH_RC=0。
This commit is contained in:
2026-10-12 15:31:03 +08:00
parent 8c651b43eb
commit 075ac6bbeb
21 changed files with 2886 additions and 185 deletions
+49 -2
View File
@@ -11,6 +11,7 @@ declare(strict_types=1);
require __DIR__ . '/app/bootstrap.php';
use App\Account;
use App\Api;
use App\Attachment;
use App\Auth;
use App\Birthday;
@@ -121,6 +122,22 @@ $r = isset($_GET['r']) ? trim((string)$_GET['r'], '/') : 'home';
$r = $r === '' ? 'home' : $r;
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
/* 🔴 v44(用户 2026-10-12 第 6 条):移动端 API 也会走到这里 ——
`?r=api/v1/tasks` 这种形式(个别网络库 / 代理会把 PATH_INFO 吃掉,
只留 query)。⇒ 必须在**登录门槛之前**交给 API 内核,否则未带 session
的 App 请求会被 302 到登录页(拿到一坨 HTML,JSON 解析失败)。
⚠️ 这里**不 require api.php**:那个文件会再 require 一次 bootstrap.php,
导致 `Constant FB_ROOT already defined` 警告(实测过)。
bootstrap 在 index.php 开头已经加载,直接调内核即可。
⚠️ 只认 `api/` 前缀,别的一律照旧走网页路由。 */
if (strpos($r, 'api/') === 0) {
// `$r` 形如 `api/v1/tasks` ⇒ 去掉 `api/` 四字、补回 `/api/` 前缀,
// 得到内核认识的标准路径 `/api/v1/tasks`(与 .htaccess/router.php 同一口径)。
$_SERVER['REQUEST_URI'] = '/api/' . substr($r, 4);
Api::dispatch();
exit;
}
$publicRoutes = ['auth/login'];
if (!Auth::check() && !in_array($r, $publicRoutes, true)) {
redirect('auth/login');
@@ -286,10 +303,17 @@ function render_me_settings(array $person, array $errors = []): void
// 通讯录同步范围要和名册页**完全一致**(用户 2026-10-06 第 22 批确认):
// 名册页看到谁就同步谁。这里把人数带过去,用户当场就能对上。
'davRosterN' => PersonRoster::countFor($familyId, $me),
/* 🔴 v44(用户 2026-10-12 第 6 条):移动端 API 令牌。
⚠️ 明文同样只在**一次性会话槽位**里取一次(`api_plain`),
取完后面 unset —— 与 DAV 口令同一条纪律。 */
'apiTokens' => \App\ApiToken::listFor($me),
'apiPlain' => (string)($_SESSION['api_plain'] ?? ''),
'apiLabel' => (string)($_SESSION['api_plain_label'] ?? ''),
'apiBase' => rtrim((string)fb_config('base_url', ''), '/'),
], 'settings');
// ⚠️ 取完即清:明文口令只该被看到一次(刷新页面就没了)。
// ⚠️ 取完即清:明文令牌只该被看到一次(刷新页面就没了)。
// 放在 View::render **之后** —— 早了就拿不到值了。
unset($_SESSION['dav_plain']);
unset($_SESSION['dav_plain'], $_SESSION['api_plain'], $_SESSION['api_plain_label']);
}
/**
@@ -4633,6 +4657,29 @@ $want = array_keys($want);
redirect('settings/me');
break;
/* 🔴 v44(用户 2026-10-12 第 6 条):移动端 API 令牌的**签发 / 吊销**。
与 DAV 那两条同一个形态(明文进一次性会话槽位、只显示一次)。 */
case 'settings/api_token':
case 'settings/api_revoke':
$me = (int)Auth::personId();
Auth::requireWrite();
csrf_check();
if ($r === 'settings/api_token') {
$label = trim((string)($_POST['label'] ?? ''));
$_SESSION['api_plain'] = \App\ApiToken::issue($me, $label);
$_SESSION['api_plain_label'] = $label;
flash('ok', 'API 令牌已生成(只显示这一次,请立刻复制到 App 里)。');
} elseif ((int)($_POST['id'] ?? 0) > 0) {
\App\ApiToken::revoke($me, (int)$_POST['id']);
flash('ok', '这枚 API 令牌已吊销。');
} else {
\App\ApiToken::revokeAll($me);
unset($_SESSION['api_plain'], $_SESSION['api_plain_label']);
flash('ok', '全部 API 令牌已吊销,所有 App 都需要重新登录。');
}
redirect('settings/me', [], 'api');
break;
case 'calendar/calendars':
$familyId = (int)Auth::familyId();
$me = (int)Auth::personId();