feat: 安全设置其它有效会话也标在线

本机仍为「当前在线」,Chrome/Edge 等同时登录的其它会话显示「在线」并可剔除。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 01:59:49 +08:00
parent 3a7bfe59df
commit b3a4b8bb03
3 changed files with 8 additions and 9 deletions

View File

@@ -72,7 +72,7 @@
- 发帖/评论:staff 直发 published,普通用户进 pending;pending 不计 comment_count(ApproveComment 时 +1,删除只减 published);公开列表仅 published;作者/staff 可经详情页看 pending/rejected(GetByIDForViewer + EnsurePostVisible) - 发帖/评论:staff 直发 published,普通用户进 pending;pending 不计 comment_count(ApproveComment 时 +1,删除只减 published);公开列表仅 published;作者/staff 可经详情页看 pending/rejected(GetByIDForViewer + EnsurePostVisible)
- 审核端点 7 个直挂 /api/admin/moderation/*;通过/拒绝发 approved/rejected 通知(拒绝需 reason,评论通过时补发 comment/reply 业务通知);ErrModerationForbidden→403、ErrNotPending→400 - 审核端点 7 个直挂 /api/admin/moderation/*;通过/拒绝发 approved/rejected 通知(拒绝需 reason,评论通过时补发 comment/reply 业务通知);ErrModerationForbidden→403、ErrNotPending→400
- 登录历史 login_logs:成功失败都记录(失败用户名可不存在 user_id=0);GET /api/admin/users/:id/login-logs;用户列表有 online(last_seen_at 5 分钟内绿点)/last_login_ip/last_login_at 列 - 登录历史 login_logs:成功失败都记录(失败用户名可不存在 user_id=0);GET /api/admin/users/:id/login-logs;用户列表有 online(last_seen_at 5 分钟内绿点)/last_login_ip/last_login_at 列
- 用户安全设置「最近登录设备」:GET /api/me/login-devices(RequireAuth,仅本人仍有效的 refresh 会话,按归一化 IP+UA 指纹去重);DELETE /api/me/login-devices/:id 剔除其它设备(吊销该指纹下除当前会话外的 refresh,JWT `fid` 使 access 立即失效,需重新输入密码)。展示设备/浏览器与原始 IP(不使用 ip2region);本机标 current=当前在线,无问号。SSR 转发 refresh cookie + 浏览器 UA/IP,直出 `/u/:id?tab=security` - 用户安全设置「最近登录设备」:GET /api/me/login-devices(RequireAuth,仅本人仍有效的 refresh 会话,按归一化 IP+UA 指纹去重);DELETE /api/me/login-devices/:id 剔除其它设备(吊销该指纹下除当前会话外的 refresh,JWT `fid` 使 access 立即失效,需重新输入密码)。展示设备/浏览器与原始 IP(不使用 ip2region);本机标 current=当前在线,其余有效会话标在线。SSR 转发 refresh cookie + 浏览器 UA/IP,直出 `/u/:id?tab=security`
- 角色变更事务内 invalidateUserSessions(token_version+1 + 撤销 refresh tokens),仅板块授权变化不下线;board_admin 授权必须 ≥1 个有效板块否则 400 - 角色变更事务内 invalidateUserSessions(token_version+1 + 撤销 refresh tokens),仅板块授权变化不下线;board_admin 授权必须 ≥1 个有效板块否则 400
- 前端:lib/roles.ts(ROLE_META:owner=Crown/--gold,super=BadgeCheck,admin=ShieldCheck,board=Shield,user=Users;lucide 无 ShieldStar)、RoleBadge 组件(truncate 用于窄列+板块名);/admin/moderation 审核队列页(待审角标 WS `moderation:changed` + 回前台 HTTP 校准,无定时轮询);Forbidden.tsx 子页守卫视图(HTTP 仍 200);用户管理 /admin/users 首列收窄 grid-cols-[minmax(0,1fr)_78px_...] - 前端:lib/roles.ts(ROLE_META:owner=Crown/--gold,super=BadgeCheck,admin=ShieldCheck,board=Shield,user=Users;lucide 无 ShieldStar)、RoleBadge 组件(truncate 用于窄列+板块名);/admin/moderation 审核队列页(待审角标 WS `moderation:changed` + 回前台 HTTP 校准,无定时轮询);Forbidden.tsx 子页守卫视图(HTTP 仍 200);用户管理 /admin/users 首列收窄 grid-cols-[minmax(0,1fr)_78px_...]
- 新用户发帖有 24h 冷静期(注册接口无此限制,API 回归需 SQL 回退 created_at);CreatePostRequest.tags 是字符串不是数组 - 新用户发帖有 24h 冷静期(注册接口无此限制,API 回归需 SQL 回退 created_at);CreatePostRequest.tags 是字符串不是数组

View File

@@ -16,7 +16,7 @@ function DeviceIcon({ kind }: { kind: LoginDevice["device_kind"] }) {
/** /**
* 安全设置:仍有效的登录会话。 * 安全设置:仍有效的登录会话。
* 本机标「当前在线」;其它设备可剔除,对方需重新输入密码登录。 * 本机标「当前在线」;其它仍有效会话标「在线」,并可剔除。
*/ */
export default function LoginDevices({ devices: initial }: { devices: LoginDevice[] }) { export default function LoginDevices({ devices: initial }: { devices: LoginDevice[] }) {
const router = useRouter(); const router = useRouter();
@@ -94,14 +94,13 @@ export default function LoginDevices({ devices: initial }: { devices: LoginDevic
<p className="text-[12px] tabular-nums leading-snug" style={{ color: "var(--ink-3)" }}> <p className="text-[12px] tabular-nums leading-snug" style={{ color: "var(--ink-3)" }}>
{formatDateTime(d.created_at)} {formatDateTime(d.created_at)}
</p> </p>
{d.current ? (
<p className="mt-1 text-[12px] font-semibold" style={{ color: "var(--ok)" }}> <p className="mt-1 text-[12px] font-semibold" style={{ color: "var(--ok)" }}>
当前在线 {d.current ? "当前在线" : "在线"}
</p> </p>
) : ( {!d.current && (
<button <button
type="button" type="button"
className="mt-1 text-[12px] font-semibold hover:underline" className="mt-0.5 text-[12px] font-semibold hover:underline"
style={{ color: "var(--danger)" }} style={{ color: "var(--danger)" }}
onClick={() => { onClick={() => {
setError(""); setError("");

View File

@@ -460,7 +460,7 @@ export default async function UserPage({ params, searchParams }: UserPageProps)
{effectiveTab === "security" && ( {effectiveTab === "security" && (
<li className="flex gap-2"> <li className="flex gap-2">
<span className="shrink-0 mt-px" style={{ color: "var(--accent)" }}>·</span> <span className="shrink-0 mt-px" style={{ color: "var(--accent)" }}>·</span>
<span>「当前在线」是你正在使用的这台设备。其它记录可剔除,对方需重新输入密码才能登录。</span> <span>「当前在线」是你正在看的这台设备;其它仍有效的登录标「在线」,可剔除后需重新输入密码。</span>
</li> </li>
)} )}
</ul> </ul>