Compare commits
2
Commits
59c72a84ce
...
953be7342d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
953be7342d | ||
|
|
28d2d5c826 |
No files matched your search
+2
-2
@@ -19,11 +19,11 @@ for example `https://cms.example/v1/article/`.
|
|||||||
or revoked key answers 401, even on public routes.
|
or revoked key answers 401, even on public routes.
|
||||||
- Each key can make 120 requests per minute. Beyond that: 429 with a `Retry-After` header.
|
- Each key can make 120 requests per minute. Beyond that: 429 with a `Retry-After` header.
|
||||||
|
|
||||||
## Routes a key can call (20)
|
## Routes a key can call (24)
|
||||||
| Group | Routes |
|
| Group | Routes |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Articles | GET /v1/article/, GET /v1/article/{id}, POST /v1/article/, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish, PATCH /v1/article/{id}/unpublish, DELETE /v1/article/{id} |
|
| Articles | GET /v1/article/, GET /v1/article/{id}, POST /v1/article/, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish, PATCH /v1/article/{id}/unpublish, DELETE /v1/article/{id} |
|
||||||
| Media | GET /v1/media/, POST /v1/media/, PATCH /v1/media/{id}, DELETE /v1/media/{id}, POST /v1/media/merge, GET /v1/media/{id}/usage |
|
| Media | GET /v1/media/, POST /v1/media/, PATCH /v1/media/{id}, DELETE /v1/media/{id}, POST /v1/media/merge, GET /v1/media/{id}/usage, GET /v1/media/purge-settings (owner or admin), PATCH /v1/media/purge-settings (owner or admin), GET /v1/media/purge-preview (owner or admin), POST /v1/media/purge (owner or admin) |
|
||||||
| Users and invites | GET /v1/user/, PATCH /v1/user/me, POST /v1/user/invite (owner only), DELETE /v1/user/invite/{id} (owner only), POST /v1/user/invite/{id}/renew (owner only), GET /v1/auth/me |
|
| Users and invites | GET /v1/user/, PATCH /v1/user/me, POST /v1/user/invite (owner only), DELETE /v1/user/invite/{id} (owner only), POST /v1/user/invite/{id}/renew (owner only), GET /v1/auth/me |
|
||||||
| Settings | GET /v1/settings |
|
| Settings | GET /v1/settings |
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ const tabItems = [
|
|||||||
color="neutral"
|
color="neutral"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:class="
|
:class="
|
||||||
endpoint.access === 'Owner only' ? CHIP_CLASS.strong : CHIP_CLASS.neutral
|
endpoint.access === 'Owner only' || endpoint.access === 'Owner or admin'
|
||||||
|
? CHIP_CLASS.strong
|
||||||
|
: CHIP_CLASS.neutral
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<UBadge
|
<UBadge
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const SUBGROUP_TITLE_CLASS =
|
|||||||
<UIcon name="i-lucide-lock" class="mt-0.5 size-4 shrink-0 text-muted" />
|
<UIcon name="i-lucide-lock" class="mt-0.5 size-4 shrink-0 text-muted" />
|
||||||
<span>{{
|
<span>{{
|
||||||
t(
|
t(
|
||||||
'These routes are made for a signed-in browser session. With an API key, verify, DELETE /auth/register, /api-key/* and /system/* answer 403 "This action needs a real session."',
|
'These routes are made for a signed-in browser session. With an API key, verify, DELETE /auth/register, PATCH /settings, /activity/*, /api-key/* and /system/* answer 403 "This action needs a real session."',
|
||||||
)
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ vi.mock('@nuxt/ui/composables', () => ({
|
|||||||
useToast: () => ({ add: vi.fn() }),
|
useToast: () => ({ add: vi.fn() }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const emptyRectList = () => Object.assign([], { item: () => null }) as unknown as DOMRectList
|
||||||
|
|
||||||
|
Range.prototype.getClientRects = emptyRectList
|
||||||
|
Range.prototype.getBoundingClientRect = () => new DOMRect()
|
||||||
|
|
||||||
function mockResponse(status: number, body: unknown = null) {
|
function mockResponse(status: number, body: unknown = null) {
|
||||||
return {
|
return {
|
||||||
ok: status >= 200 && status < 300,
|
ok: status >= 200 && status < 300,
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ const EXPECTED_GROUPS: Record<string, string[]> = {
|
|||||||
'DELETE /v1/media/{id}',
|
'DELETE /v1/media/{id}',
|
||||||
'POST /v1/media/merge',
|
'POST /v1/media/merge',
|
||||||
'GET /v1/media/{id}/usage',
|
'GET /v1/media/{id}/usage',
|
||||||
|
'GET /v1/media/purge-settings',
|
||||||
|
'PATCH /v1/media/purge-settings',
|
||||||
|
'GET /v1/media/purge-preview',
|
||||||
|
'POST /v1/media/purge',
|
||||||
],
|
],
|
||||||
'Users and invites': [
|
'Users and invites': [
|
||||||
'GET /v1/user/',
|
'GET /v1/user/',
|
||||||
@@ -61,6 +65,8 @@ const EXPECTED_SESSION: Record<string, string[]> = {
|
|||||||
'DELETE /v1/auth/register',
|
'DELETE /v1/auth/register',
|
||||||
'POST /v1/auth/password-reset',
|
'POST /v1/auth/password-reset',
|
||||||
],
|
],
|
||||||
|
'Settings · owner only': ['PATCH /v1/settings'],
|
||||||
|
'Activity log': ['GET /v1/activity', 'GET /v1/activity/summary', 'GET /v1/activity/export'],
|
||||||
'API key management': ['GET /v1/api-key', 'POST /v1/api-key', 'DELETE /v1/api-key/{id}'],
|
'API key management': ['GET /v1/api-key', 'POST /v1/api-key', 'DELETE /v1/api-key/{id}'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,20 +139,20 @@ function expectOnlyKeys(value: unknown, allowed: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Catalogue routes', () => {
|
describe('Catalogue routes', () => {
|
||||||
it('holds exactly the 20 key-callable routes in 4 groups, in order', () => {
|
it('holds exactly the 24 key-callable routes in 4 groups, in order', () => {
|
||||||
expect(
|
expect(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
API_GROUPS.map((group) => [group.title, group.endpoints.map(label)]),
|
API_GROUPS.map((group) => [group.title, group.endpoints.map(label)]),
|
||||||
),
|
),
|
||||||
).toEqual(EXPECTED_GROUPS)
|
).toEqual(EXPECTED_GROUPS)
|
||||||
expect(allEndpoints()).toHaveLength(20)
|
expect(allEndpoints()).toHaveLength(24)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('holds the 13 session-only rows in 3 sub-groups', () => {
|
it('holds the 17 session-only rows in 5 sub-groups', () => {
|
||||||
expect(
|
expect(
|
||||||
Object.fromEntries(SESSION_GROUPS.map((group) => [group.title, group.rows.map(label)])),
|
Object.fromEntries(SESSION_GROUPS.map((group) => [group.title, group.rows.map(label)])),
|
||||||
).toEqual(EXPECTED_SESSION)
|
).toEqual(EXPECTED_SESSION)
|
||||||
expect(SESSION_GROUPS.flatMap((group) => group.rows)).toHaveLength(13)
|
expect(SESSION_GROUPS.flatMap((group) => group.rows)).toHaveLength(17)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('holds the 6 public auth flow rows', () => {
|
it('holds the 6 public auth flow rows', () => {
|
||||||
@@ -168,7 +174,7 @@ describe('Catalogue routes', () => {
|
|||||||
),
|
),
|
||||||
...PUBLIC_AUTH_ROWS.map((item) => [label(item), item.description] as const),
|
...PUBLIC_AUTH_ROWS.map((item) => [label(item), item.description] as const),
|
||||||
]
|
]
|
||||||
expect(described).toHaveLength(4 + 20 + 13 + 6)
|
expect(described).toHaveLength(4 + 24 + 17 + 6)
|
||||||
const blank = described.filter(([, description]) => description.trim() === '')
|
const blank = described.filter(([, description]) => description.trim() === '')
|
||||||
expect(blank.map(([name]) => name)).toEqual([])
|
expect(blank.map(([name]) => name)).toEqual([])
|
||||||
})
|
})
|
||||||
@@ -190,7 +196,9 @@ describe('Catalogue completeness', () => {
|
|||||||
const examples = endpointExamples(endpoint, ORIGIN)
|
const examples = endpointExamples(endpoint, ORIGIN)
|
||||||
expect(['GET', 'POST', 'PATCH', 'DELETE']).toContain(endpoint.method)
|
expect(['GET', 'POST', 'PATCH', 'DELETE']).toContain(endpoint.method)
|
||||||
expect(endpoint.path.startsWith('/v1/')).toBe(true)
|
expect(endpoint.path.startsWith('/v1/')).toBe(true)
|
||||||
expect(['Public', 'Verified user', 'Owner only']).toContain(endpoint.access)
|
expect(['Public', 'Verified user', 'Owner or admin', 'Owner only']).toContain(
|
||||||
|
endpoint.access,
|
||||||
|
)
|
||||||
expect(examples.fetch).toContain(`${ORIGIN}/v1/`)
|
expect(examples.fetch).toContain(`${ORIGIN}/v1/`)
|
||||||
expect(examples.curl).toContain(`${ORIGIN}/v1/`)
|
expect(examples.curl).toContain(`${ORIGIN}/v1/`)
|
||||||
expect(examples.curl).toContain(
|
expect(examples.curl).toContain(
|
||||||
@@ -227,6 +235,30 @@ describe('Catalogue completeness', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Owner or admin purge routes', () => {
|
||||||
|
it('gives Owner or admin to the four purge routes only', () => {
|
||||||
|
expect(
|
||||||
|
allEndpoints()
|
||||||
|
.filter((endpoint) => endpoint.access === 'Owner or admin')
|
||||||
|
.map(label),
|
||||||
|
).toEqual([
|
||||||
|
'GET /v1/media/purge-settings',
|
||||||
|
'PATCH /v1/media/purge-settings',
|
||||||
|
'GET /v1/media/purge-preview',
|
||||||
|
'POST /v1/media/purge',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lists the owner or admin 403 on each purge route', () => {
|
||||||
|
for (const endpoint of allEndpoints().filter((item) => item.access === 'Owner or admin')) {
|
||||||
|
expect(endpoint.errors).toContainEqual({
|
||||||
|
status: 403,
|
||||||
|
caption: 'Only owners and admins can do this.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Response samples use the real domain JSON keys', () => {
|
describe('Response samples use the real domain JSON keys', () => {
|
||||||
it('uses the article keys, without images or status', () => {
|
it('uses the article keys, without images or status', () => {
|
||||||
const single = parsed(find('GET /v1/article/{id}'))
|
const single = parsed(find('GET /v1/article/{id}'))
|
||||||
@@ -286,8 +318,14 @@ describe('Response samples use the real domain JSON keys', () => {
|
|||||||
expectOnlyKeys(parsed(find('PATCH /v1/user/me')), ME_KEYS)
|
expectOnlyKeys(parsed(find('PATCH /v1/user/me')), ME_KEYS)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses version for the settings', () => {
|
it('uses the version and activity retention keys for the settings', () => {
|
||||||
expect(keysOf(parsed(find('GET /v1/settings')))).toEqual(['version'])
|
expect(keysOf(parsed(find('GET /v1/settings')))).toEqual([
|
||||||
|
'activity_last_cleanup_at',
|
||||||
|
'activity_last_cleanup_removed',
|
||||||
|
'activity_next_cleanup_at',
|
||||||
|
'activity_retention_days',
|
||||||
|
'version',
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('answers 204 for the three deletes without a JSON body', () => {
|
it('answers 204 for the three deletes without a JSON body', () => {
|
||||||
@@ -456,7 +494,7 @@ describe('Per-endpoint error captions use the real strings', () => {
|
|||||||
describe('Session only rows', () => {
|
describe('Session only rows', () => {
|
||||||
const rows = SESSION_GROUPS.flatMap((group) => group.rows)
|
const rows = SESSION_GROUPS.flatMap((group) => group.rows)
|
||||||
|
|
||||||
it('claims 403 only on verify, DELETE register, api-key and system rows', () => {
|
it('claims 403 only on verify, DELETE register, settings, activity, api-key and system rows', () => {
|
||||||
expect(rows.filter((row) => row.answers403).map(label)).toEqual([
|
expect(rows.filter((row) => row.answers403).map(label)).toEqual([
|
||||||
'GET /v1/system/update',
|
'GET /v1/system/update',
|
||||||
'POST /v1/system/update/check',
|
'POST /v1/system/update/check',
|
||||||
@@ -464,6 +502,10 @@ describe('Session only rows', () => {
|
|||||||
'POST /v1/system/update/restart',
|
'POST /v1/system/update/restart',
|
||||||
'POST /v1/auth/verify',
|
'POST /v1/auth/verify',
|
||||||
'DELETE /v1/auth/register',
|
'DELETE /v1/auth/register',
|
||||||
|
'PATCH /v1/settings',
|
||||||
|
'GET /v1/activity',
|
||||||
|
'GET /v1/activity/summary',
|
||||||
|
'GET /v1/activity/export',
|
||||||
'GET /v1/api-key',
|
'GET /v1/api-key',
|
||||||
'POST /v1/api-key',
|
'POST /v1/api-key',
|
||||||
'DELETE /v1/api-key/{id}',
|
'DELETE /v1/api-key/{id}',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'
|
export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'
|
||||||
export type AccessLevel = 'Public' | 'Verified user' | 'Owner only'
|
export type AccessLevel = 'Public' | 'Verified user' | 'Owner or admin' | 'Owner only'
|
||||||
|
|
||||||
export interface ApiField {
|
export interface ApiField {
|
||||||
name: string
|
name: string
|
||||||
@@ -72,6 +72,7 @@ export const API_KEY_PLACEHOLDER = 'rcms_Kf3x9QwL0aBc…'
|
|||||||
export const NO_CONTENT_RESPONSE = '204 No Content'
|
export const NO_CONTENT_RESPONSE = '204 No Content'
|
||||||
export const SESSION_REQUIRED_MESSAGE = 'This action needs a real session.'
|
export const SESSION_REQUIRED_MESSAGE = 'This action needs a real session.'
|
||||||
export const OWNER_REQUIRED_MESSAGE = 'Only the owner can do this.'
|
export const OWNER_REQUIRED_MESSAGE = 'Only the owner can do this.'
|
||||||
|
export const OWNER_OR_ADMIN_MESSAGE = 'Only owners and admins can do this.'
|
||||||
export const LOGIN_FAMILY_CAPTION = 'Made for the browser. A key is ignored here, with no 403.'
|
export const LOGIN_FAMILY_CAPTION = 'Made for the browser. A key is ignored here, with no 403.'
|
||||||
export const RATE_LIMIT_SENTENCE = 'This route is rate limited. Too many requests answer 429.'
|
export const RATE_LIMIT_SENTENCE = 'This route is rate limited. Too many requests answer 429.'
|
||||||
|
|
||||||
@@ -81,6 +82,10 @@ const VERIFY_REQUIRED = 'You must verify your account to access this resource.'
|
|||||||
const ID_FORMAT =
|
const ID_FORMAT =
|
||||||
'The server expects the ID to be in the format of an unsigned 32-bit integer (uint32).'
|
'The server expects the ID to be in the format of an unsigned 32-bit integer (uint32).'
|
||||||
const FORMAT_INVALID = 'The format must be one of: markdown, html, json.'
|
const FORMAT_INVALID = 'The format must be one of: markdown, html, json.'
|
||||||
|
const PURGE_INTERVAL = 'Enter a whole number of days between 1 and 365.'
|
||||||
|
const PURGE_CONFLICT =
|
||||||
|
'The library changed, so nothing was purged. Check the new summary and try again.'
|
||||||
|
const PURGE_BUSY = 'A purge is already running.'
|
||||||
const TOO_MANY_REQUESTS = 'Too many requests. Please try again later.'
|
const TOO_MANY_REQUESTS = 'Too many requests. Please try again later.'
|
||||||
|
|
||||||
const FORMAT_QUERY: ApiField = {
|
const FORMAT_QUERY: ApiField = {
|
||||||
@@ -177,10 +182,26 @@ const articleErrors = (...own: ApiError[]) => [...own, e(400, FORMAT_INVALID), e
|
|||||||
|
|
||||||
const verified = (errors: ApiError[]) => [...errors, e(403, VERIFY_REQUIRED)]
|
const verified = (errors: ApiError[]) => [...errors, e(403, VERIFY_REQUIRED)]
|
||||||
|
|
||||||
|
const ownerOrAdminErrors = (...own: ApiError[]) => [
|
||||||
|
...own,
|
||||||
|
e(401, UNAUTHORIZED),
|
||||||
|
e(403, OWNER_OR_ADMIN_MESSAGE),
|
||||||
|
]
|
||||||
|
|
||||||
const articleIdErrors = [e(400, ID_FORMAT), e(404, 'Article not found')]
|
const articleIdErrors = [e(400, ID_FORMAT), e(404, 'Article not found')]
|
||||||
|
|
||||||
const none: ApiField[] = []
|
const none: ApiField[] = []
|
||||||
|
|
||||||
|
const PURGE_SETTINGS = {
|
||||||
|
auto_purge_enabled: true,
|
||||||
|
auto_purge_interval_days: 30,
|
||||||
|
last_purge_at: '2026-09-10T07:00:00Z',
|
||||||
|
last_purge_merged: 2,
|
||||||
|
last_purge_articles_updated: 5,
|
||||||
|
enabled_at: '2026-08-11T07:00:00Z',
|
||||||
|
next_purge_at: '2026-10-09T07:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
function endpoint(
|
function endpoint(
|
||||||
id: string,
|
id: string,
|
||||||
method: HttpMethod,
|
method: HttpMethod,
|
||||||
@@ -479,6 +500,97 @@ export const API_GROUPS: ApiGroup[] = [
|
|||||||
json([{ id: 12, title: ARTICLE_TITLE, published: true }]),
|
json([{ id: 12, title: ARTICLE_TITLE, published: true }]),
|
||||||
{ pathId: 4 },
|
{ pathId: 4 },
|
||||||
),
|
),
|
||||||
|
endpoint(
|
||||||
|
'med-purge-get',
|
||||||
|
'GET',
|
||||||
|
'/v1/media/purge-settings',
|
||||||
|
'Get the automatic duplicate purge settings.',
|
||||||
|
'Owner or admin',
|
||||||
|
ownerOrAdminErrors(),
|
||||||
|
json(PURGE_SETTINGS),
|
||||||
|
),
|
||||||
|
endpoint(
|
||||||
|
'med-purge-set',
|
||||||
|
'PATCH',
|
||||||
|
'/v1/media/purge-settings',
|
||||||
|
'Turn the automatic purge on or off, or change its interval.',
|
||||||
|
'Owner or admin',
|
||||||
|
ownerOrAdminErrors(e(400, BODY_MISMATCH), e(400, PURGE_INTERVAL)),
|
||||||
|
json(PURGE_SETTINGS),
|
||||||
|
{
|
||||||
|
bodyKind: 'json',
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
name: 'auto_purge_enabled',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
rule: 'Leave out to keep the current value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'auto_purge_interval_days',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
rule: '1 to 365. Leave out to keep the current value.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bodySample: { auto_purge_enabled: true, auto_purge_interval_days: 30 },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
endpoint(
|
||||||
|
'med-purge-prev',
|
||||||
|
'GET',
|
||||||
|
'/v1/media/purge-preview',
|
||||||
|
'Preview what a purge would merge. Nothing changes.',
|
||||||
|
'Owner or admin',
|
||||||
|
ownerOrAdminErrors(),
|
||||||
|
json({
|
||||||
|
groups: 1,
|
||||||
|
copies: 2,
|
||||||
|
articles: 5,
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
keep_id: 4,
|
||||||
|
filename: 'north-wall.jpg',
|
||||||
|
url: '/static/uploadedImages/3f9c2b7e-5d1a-4c8e-9b0f-6a2d4e8c1b73.jpg',
|
||||||
|
used_in: 5,
|
||||||
|
merged: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
more: 0,
|
||||||
|
fingerprint: '9c1f5e0a7d3b',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
endpoint(
|
||||||
|
'med-purge-run',
|
||||||
|
'POST',
|
||||||
|
'/v1/media/purge',
|
||||||
|
'Merge every group of identical copies. Send the preview fingerprint to refuse a changed library.',
|
||||||
|
'Owner or admin',
|
||||||
|
ownerOrAdminErrors(
|
||||||
|
e(400, BODY_MISMATCH),
|
||||||
|
e(409, PURGE_CONFLICT),
|
||||||
|
e(409, PURGE_BUSY),
|
||||||
|
),
|
||||||
|
json({
|
||||||
|
groups_total: 1,
|
||||||
|
groups_purged: 1,
|
||||||
|
merged: 2,
|
||||||
|
articles_updated: 5,
|
||||||
|
failed_groups: 0,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
bodyKind: 'json',
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
name: 'fingerprint',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
rule: 'The fingerprint of the preview. Without it, the purge runs on the current library.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bodySample: { fingerprint: '9c1f5e0a7d3b' },
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -598,7 +710,7 @@ export const API_GROUPS: ApiGroup[] = [
|
|||||||
{
|
{
|
||||||
key: 'settings',
|
key: 'settings',
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
description: 'Read the site settings.',
|
description: 'Read the site settings, including the activity log retention.',
|
||||||
endpoints: [
|
endpoints: [
|
||||||
endpoint(
|
endpoint(
|
||||||
'set-get',
|
'set-get',
|
||||||
@@ -607,7 +719,13 @@ export const API_GROUPS: ApiGroup[] = [
|
|||||||
'Get settings.',
|
'Get settings.',
|
||||||
'Verified user',
|
'Verified user',
|
||||||
verified([e(401, UNAUTHORIZED)]),
|
verified([e(401, UNAUTHORIZED)]),
|
||||||
json({ version: 'v2.2.0-beta' }),
|
json({
|
||||||
|
version: 'v3.0.0-beta',
|
||||||
|
activity_retention_days: 90,
|
||||||
|
activity_last_cleanup_at: '2026-09-10T03:00:00Z',
|
||||||
|
activity_last_cleanup_removed: 12,
|
||||||
|
activity_next_cleanup_at: '2026-09-11T03:00:00Z',
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -646,6 +764,18 @@ export const SESSION_GROUPS: SessionGroup[] = [
|
|||||||
row('POST', '/v1/auth/password-reset', 'Reset password', false),
|
row('POST', '/v1/auth/password-reset', 'Reset password', false),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Settings · owner only',
|
||||||
|
rows: [row('PATCH', '/v1/settings', 'Change the activity log retention')],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Activity log',
|
||||||
|
rows: [
|
||||||
|
row('GET', '/v1/activity', 'List activity events'),
|
||||||
|
row('GET', '/v1/activity/summary', 'Activity summary, owner and admin'),
|
||||||
|
row('GET', '/v1/activity/export', 'Export activity as CSV, owner and admin'),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'API key management',
|
title: 'API key management',
|
||||||
rows: [
|
rows: [
|
||||||
|
|||||||
@@ -905,13 +905,16 @@ export const messages = {
|
|||||||
'On this page': 'Sur cette page',
|
'On this page': 'Sur cette page',
|
||||||
'Jump to section': 'Aller à la section',
|
'Jump to section': 'Aller à la section',
|
||||||
'Jump to': 'Aller à',
|
'Jump to': 'Aller à',
|
||||||
'These routes are made for a signed-in browser session. With an API key, verify, DELETE /auth/register, /api-key/* and /system/* answer 403 "This action needs a real session."':
|
'These routes are made for a signed-in browser session. With an API key, verify, DELETE /auth/register, PATCH /settings, /activity/*, /api-key/* and /system/* answer 403 "This action needs a real session."':
|
||||||
'Ces routes sont prévues pour une session de navigateur connectée. Avec une clé d’API, verify, DELETE /auth/register, /api-key/* et /system/* répondent 403 « This action needs a real session. »',
|
'Ces routes sont prévues pour une session de navigateur connectée. Avec une clé d’API, verify, DELETE /auth/register, PATCH /settings, /activity/*, /api-key/* et /system/* répondent 403 « This action needs a real session. »',
|
||||||
'Session only': 'Session uniquement',
|
'Session only': 'Session uniquement',
|
||||||
'Public auth flow': 'Parcours d’authentification public',
|
'Public auth flow': 'Parcours d’authentification public',
|
||||||
Public: 'Public',
|
Public: 'Public',
|
||||||
'Verified user': 'Utilisateur vérifié',
|
'Verified user': 'Utilisateur vérifié',
|
||||||
'Owner only': 'Propriétaire uniquement',
|
'Owner only': 'Propriétaire uniquement',
|
||||||
|
'Owner or admin': 'Propriétaire ou administrateur',
|
||||||
|
'Settings · owner only': 'Paramètres · propriétaire uniquement',
|
||||||
|
'Activity log': 'Journal d’activité',
|
||||||
Authentication: 'Authentification',
|
Authentication: 'Authentification',
|
||||||
Notes: 'Notes',
|
Notes: 'Notes',
|
||||||
'Users and invites': 'Utilisateurs et invitations',
|
'Users and invites': 'Utilisateurs et invitations',
|
||||||
|
|||||||
@@ -170,14 +170,14 @@ describe('Default, every card collapsed', () => {
|
|||||||
expect(row(wrapper, 'POST /v1/article/').text()).toContain('POST')
|
expect(row(wrapper, 'POST /v1/article/').text()).toContain('POST')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('groups the 20 cards as 7, 6, 6 and 1', async () => {
|
it('groups the 24 cards as 7, 10, 6 and 1', async () => {
|
||||||
const wrapper = await mountIntegration()
|
const wrapper = await mountIntegration()
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
['articles', 'media', 'users', 'settings'].map(
|
['articles', 'media', 'users', 'settings'].map(
|
||||||
(id) => section(wrapper, id).findAllComponents(EndpointCard).length,
|
(id) => section(wrapper, id).findAllComponents(EndpointCard).length,
|
||||||
),
|
),
|
||||||
).toEqual([7, 6, 6, 1])
|
).toEqual([7, 10, 6, 1])
|
||||||
expect(section(wrapper, 'articles').text()).toContain('7 routes')
|
expect(section(wrapper, 'articles').text()).toContain('7 routes')
|
||||||
expect(section(wrapper, 'settings').text()).toContain('1 route')
|
expect(section(wrapper, 'settings').text()).toContain('1 route')
|
||||||
})
|
})
|
||||||
@@ -361,16 +361,16 @@ describe('Authentication', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Session only', () => {
|
describe('Session only', () => {
|
||||||
it('renders the banner, 13 rows without expand controls and the 403 chips', async () => {
|
it('renders the banner, 17 rows without expand controls and the 403 chips', async () => {
|
||||||
const wrapper = await mountIntegration()
|
const wrapper = await mountIntegration()
|
||||||
const session = section(wrapper, 'session')
|
const session = section(wrapper, 'session')
|
||||||
|
|
||||||
expect(session.text()).toContain('13 routes')
|
expect(session.text()).toContain('17 routes')
|
||||||
expect(session.text()).toContain(
|
expect(session.text()).toContain(
|
||||||
'These routes are made for a signed-in browser session. With an API key, verify, DELETE /auth/register, /api-key/* and /system/* answer 403 "This action needs a real session."',
|
'These routes are made for a signed-in browser session. With an API key, verify, DELETE /auth/register, PATCH /settings, /activity/*, /api-key/* and /system/* answer 403 "This action needs a real session."',
|
||||||
)
|
)
|
||||||
const rows = session.findAll('[data-session-row]')
|
const rows = session.findAll('[data-session-row]')
|
||||||
expect(rows).toHaveLength(13)
|
expect(rows).toHaveLength(17)
|
||||||
expect(session.findAll('[data-session-row] button')).toHaveLength(0)
|
expect(session.findAll('[data-session-row] button')).toHaveLength(0)
|
||||||
expect(session.findAll('[data-session-row] [aria-expanded]')).toHaveLength(0)
|
expect(session.findAll('[data-session-row] [aria-expanded]')).toHaveLength(0)
|
||||||
|
|
||||||
@@ -382,6 +382,10 @@ describe('Session only', () => {
|
|||||||
'POST /v1/system/update/restart',
|
'POST /v1/system/update/restart',
|
||||||
'POST /v1/auth/verify',
|
'POST /v1/auth/verify',
|
||||||
'DELETE /v1/auth/register',
|
'DELETE /v1/auth/register',
|
||||||
|
'PATCH /v1/settings',
|
||||||
|
'GET /v1/activity',
|
||||||
|
'GET /v1/activity/summary',
|
||||||
|
'GET /v1/activity/export',
|
||||||
'GET /v1/api-key',
|
'GET /v1/api-key',
|
||||||
'POST /v1/api-key',
|
'POST /v1/api-key',
|
||||||
'DELETE /v1/api-key/{id}',
|
'DELETE /v1/api-key/{id}',
|
||||||
|
|||||||
Reference in new issue
Block a user