feat(api): 会话记录设备与最近活动,账户页可查看并退出任一设备,删除 passkey 或更换邮箱时退出其它设备,staff 会话空闲 30 分钟失效,吊销会话校验所属用户
This commit is contained in:
40 files changed
+2139
-179
No files matched your search
+124
-6
@@ -574,16 +574,38 @@ components:
|
||||
|
||||
SessionView:
|
||||
type: object
|
||||
description: One live session of a user visible to an admin (internal/api/repo.go SessionView).
|
||||
required: [token_hash, created_at, expires_at]
|
||||
description: >-
|
||||
One live session, as the account holder and an admin see it
|
||||
(internal/api/repo.go SessionView).
|
||||
required: [token_hash, created_at, expires_at, last_seen_at, user_agent, client_ip]
|
||||
properties:
|
||||
token_hash: { type: string }
|
||||
token_hash:
|
||||
type: string
|
||||
description: The sha-256 of the session cookie; the id the revoke routes take.
|
||||
created_at: { type: string, format: date-time }
|
||||
expires_at: { type: string, format: date-time }
|
||||
last_seen_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: >-
|
||||
When the session last authenticated a request, recorded at most once a
|
||||
minute. A staff session idle for 30 minutes stops authenticating and
|
||||
leaves the list.
|
||||
user_agent:
|
||||
type: string
|
||||
description: The browser's User-Agent at sign-in (at most 256 bytes; empty when none was sent).
|
||||
client_ip:
|
||||
type: string
|
||||
description: The address the sign-in came from (empty when unknown).
|
||||
revoked_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Present only once the session is revoked.
|
||||
current:
|
||||
type: boolean
|
||||
description: >-
|
||||
On the holder's own list only, true on the session the request came in
|
||||
on. Absent otherwise.
|
||||
|
||||
paths:
|
||||
# ----------------------------------------------------------------- health ---
|
||||
@@ -3693,7 +3715,7 @@ paths:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Live (unrevoked, unexpired) sessions, newest first.
|
||||
description: Live sessions, most recently seen first.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -3756,6 +3778,13 @@ paths:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
description: >-
|
||||
session_not_found — the hash is not a live session of this user (another
|
||||
user's, already ended, or unknown). Nothing is revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Error' }
|
||||
|
||||
/api/v1/users/{id}/passkeys:
|
||||
delete:
|
||||
@@ -3998,7 +4027,10 @@ paths:
|
||||
summary: Redeem an email one-time code and mark the caller's email verified (spec §B2).
|
||||
description: >
|
||||
Consumes a previously delivered code for the authenticated principal. On
|
||||
success the user's email is written and email_verified is set true. Too many
|
||||
success the user's email is written and email_verified is set true. When the
|
||||
new address replaces a different verified one, every other session of the
|
||||
caller is signed out: sign-in codes now go to the new address, so a session
|
||||
opened through the old one ends. Too many
|
||||
incorrect attempts lock the code (429 otp_locked); 10 wrong codes in 24h,
|
||||
counted across every code, lock the account's email-code door until the
|
||||
window ends (429 otp_account_locked with Retry-After). An unknown, expired,
|
||||
@@ -4198,7 +4230,8 @@ paths:
|
||||
unbind their OWN credential. An unknown or cross-user id is a 404; it never
|
||||
silently no-ops as success. The account's only passkey cannot be removed while
|
||||
its email is unverified (409 last_passkey): it is then the account's only
|
||||
durable way in.
|
||||
durable way in. Removing a passkey signs out every other session of the
|
||||
caller, so a session opened with that passkey ends with it.
|
||||
x-felis-face: [external]
|
||||
x-felis-tier: app
|
||||
security: [{ accessJWT: [] }]
|
||||
@@ -4224,6 +4257,91 @@ paths:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Error' }
|
||||
|
||||
/api/v1/account/sessions:
|
||||
get:
|
||||
tags: [account]
|
||||
operationId: listMySessions
|
||||
summary: List the caller's own live sessions, marking the one this request came in on.
|
||||
description: >
|
||||
Every device signed in to the caller's account, most recently seen first. A
|
||||
caller signed in through Cloudflare Access has no session of its own, so no
|
||||
entry is marked current.
|
||||
x-felis-face: [external]
|
||||
x-felis-tier: app
|
||||
security: [{ accessJWT: [] }]
|
||||
responses:
|
||||
'200':
|
||||
description: The caller's live sessions.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [sessions]
|
||||
properties:
|
||||
sessions:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/SessionView' }
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
|
||||
/api/v1/account/sessions/{hash}:
|
||||
delete:
|
||||
tags: [account]
|
||||
operationId: revokeMySession
|
||||
summary: Sign out one of the caller's sessions.
|
||||
description: >
|
||||
Scoped to the caller: a hash that is not one of the caller's live sessions is
|
||||
a 404 whoever it belongs to. Revoking the session the request came in on is
|
||||
a sign-out; the cookie is cleared and signed_out is true.
|
||||
x-felis-face: [external]
|
||||
x-felis-tier: app
|
||||
security: [{ accessJWT: [] }]
|
||||
parameters:
|
||||
- { name: hash, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Session revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [ok, signed_out]
|
||||
properties:
|
||||
ok: { type: boolean, const: true }
|
||||
signed_out:
|
||||
type: boolean
|
||||
description: True when the revoked session was the caller's own, which is now signed out.
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
description: session_not_found — not a live session of the caller.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Error' }
|
||||
|
||||
/api/v1/account/sessions/revoke-others:
|
||||
post:
|
||||
tags: [account]
|
||||
operationId: revokeMyOtherSessions
|
||||
summary: Sign out every session of the caller except the one making this request.
|
||||
x-felis-face: [external]
|
||||
x-felis-tier: app
|
||||
security: [{ accessJWT: [] }]
|
||||
responses:
|
||||
'200':
|
||||
description: Other sessions revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [revoked]
|
||||
properties:
|
||||
revoked:
|
||||
type: integer
|
||||
description: How many sessions were signed out.
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
|
||||
/api/v1/account/migrate:
|
||||
get:
|
||||
tags: [account]
|
||||
|
||||
Reference in new issue
Block a user