@ -142,7 +142,7 @@ async function routes(app: FastifyInstance) {
schema:{
summary:'Revoke an API key',
description:
'Permanent: the key stays listed as revoked and stops authenticating on the next request. Keys are never deleted, so the record of what existed is kept.',
'Permanent: the key stays listed as revoked and stops authenticating on the next request. Revoking never deletes, so the record of what existed is kept — `POST /system/api-keys/purge` is what discards those rows, when an administrator asks for it.',
"Issued before the signing certificates were last regenerated, so its signature no longer verifies. Not a state anybody set — it is the key's age against the keypair's, and unlike revocation it applies to every key at once."
@ -295,7 +298,7 @@ async function routes(app: FastifyInstance) {
schema:{
summary:'Get the security configuration',
description:
'The JWT fields come from the `auth` settings, which are the ones actually in force. Most of the rest is applied when the HTTP server starts, so changing it takes effect on the next restart.',
'Most of this is applied when the HTTP server starts, so changing it takes effect on the next restart.',
tags:['System'],
response:{
200:{$ref:'SecurityConfig#'}
@ -319,7 +322,7 @@ async function routes(app: FastifyInstance) {
schema:{
summary:'Update the security configuration',
description:
'Accepts any subset of the fields. Changing the JWT audience invalidates every API key already issued, since a key carries the audience it was signed with. Header, CORS and proxy settings are read when the HTTP server starts and therefore apply after a restart.',
'Accepts any subset of the fields. Header, CORS and proxy settings are read when the HTTP server starts and therefore apply after a restart.',
tags:['System'],
body:{$ref:'SecurityConfig#'},
response:{
@ -884,6 +887,329 @@ async function routes(app: FastifyInstance) {
}
)
/**
*DISCONNECTWEBSOCKETSESSIONS
*/
app.post(
'/websockets/disconnect',
{
config:{
permissions:['manage:system']
},
schema:{
summary:'Close every websocket connection, on every instance',
description:
'The sockets are the editors of live collaborative editing (`/_collab`) and the admin terminal’s log stream (`/_terminal`). Closing one is not a refusal: the code sent is a plain "come back", so an editor reconnects on its own and picks up the room it was in, and its unsaved text survives as long as somebody else is still in that room. Every other instance is told to do the same over the event bus, and does it as it hears it — `count` is this instance’s own, since a socket is held by the instance the browser reached and nothing reports back.',
description:'Connections that were open on this instance and have been closed.'
}
}
}
}
}
},
async()=>{
constcount=maintenance.disconnectWebsockets()
WIKI.events.outbound.emit('disconnectWebsockets')
return{
ok: true,
message:`Closed ${count} websocket connection(s) on this instance.`,
count
}
}
)
/**
*FLUSHCACHE
*/
app.post(
'/cache/flush',
{
config:{
permissions:['manage:system']
},
schema:{
summary:'Flush the caches, on every instance',
description:
'Throws away everything an instance holds that the database is the real copy of: the file and icon caches, in memory and on disk, and the site, group, page-rule and locale state that answers every request. Nothing is lost and nothing is disabled — what is read on every request is refilled before this answers, and the rest as it is asked for again. Every other instance is told to do the same over the event bus, and does it as it hears it.',
tags:['System'],
response:{
200:{
description:'Cache flushed successfully',
type:'object',
properties:{
ok:{
type:'boolean'
},
message:{
type:'string'
}
}
}
}
}
},
async()=>{
awaitmaintenance.flushCaches()
WIKI.events.outbound.emit('flushCaches')
return{
ok: true,
message:'The cache has been flushed.'
}
}
)
/**
*GETAPIKEYCERTIFICATESTATE
*/
app.get(
'/certificates',
{
config:{
permissions:['manage:system']
},
schema:{
summary:'When the API key signing certificates were generated',
description:
'The moment the current keypair came into being — at install, or the last time an administrator regenerated it. Every key issued before it was signed by a keypair that no longer exists and cannot authenticate, which is what `isInvalidated` on a key reports.',
summary:'Replace the API key signing certificates',
description:
'Generates a new keypair and a new passphrase for it. An API key is a token signed with that keypair, so every key ever issued stops authenticating at once, on every instance — this is what takes back a key that has escaped and cannot be revoked one at a time. The key rows are left as they are, still listed and still not revoked: what has to happen next is that each one is reissued. Logins are unaffected — session cookies are signed with a secret of their own.',
returnreply.internalServerError('Failed to save the new certificates.')
}
return{
ok: true,
message:`Certificates regenerated successfully. ${invalidatedKeys} API key(s) will have to be reissued.`,
invalidatedKeys
}
}
)
/**
*PURGEREVOKEDAPIKEYS
*/
app.post(
'/api-keys/purge',
{
config:{
permissions:['manage:system']
},
schema:{
summary:'Delete every revoked API key',
description:
'Clears revoked keys out of the list for good. Nothing about access changes — a revoked key already authenticates nothing — so this trades the record that the key ever existed for a shorter list. Keys that are merely invalidated are kept: one of those is a key nobody has made a decision about, and its row is what tells its owner to reissue it.',
summary:'Rotate the session secret and end every session',
description:
'Logs everybody out, this caller included, and gives @fastify/session a new secret to sign cookies with. The two happen together on purpose: ending the sessions takes effect immediately and everywhere, since they are rows every instance shares, while the new secret is only picked up when an instance restarts — the plugins are handed it at startup. API keys are unaffected; their keypair carries its own passphrase.',
tags:['System'],
response:{
200:{
description:'Sessions invalidated successfully',
type:'object',
properties:{
ok:{
type:'boolean'
},
message:{
type:'string'
},
count:{
type:'number',
description:'Sessions that were open and have been ended.'
message:`Ended ${count} session(s) and rotated the session secret.`,
count
}
}
)
/**
*PURGEPAGEHISTORY
*/
app.post<{Body:{olderThan: PurgeTimeframe}}>(
'/history/purge',
{
config:{
permissions:['manage:system']
},
schema:{
summary:'Purge page history older than a timeframe',
description:
'Deletes every version older than the cutoff, on every site. Pages themselves are untouched — a page row holds what it says now — so this shortens timelines and takes away what a page can be rolled back to, nothing more. With one exception: the versions of a page that was DELETED are all that is left of it, so purging past the day it went is what finally discards it. Nothing here can be undone.',
tags:['System'],
body:{
type:'object',
required:['olderThan'],
properties:{
olderThan:{
type:'string',
enum:Object.keys(purgeTimeframes),
description:'How far back to keep. Everything older than this is deleted.'
"admin.api.invalidatedHint":"This key was issued before the API keys certificates were regenerated on {date}, so it can no longer be used. Create a new key to replace it.",
"admin.api.key":"API Key",
"admin.api.keyEndingIn":"Ending in {suffix}",
"admin.api.loadFailed":"Failed to load API keys.",
@ -740,9 +742,6 @@
"admin.security.hsts":"HSTS (HTTP Strict Transport Security)",
"admin.security.hstsDuration":"HSTS Max Age",
"admin.security.hstsDurationHint":"Defines the duration for which the server should only deliver content through HTTPS. It's a good idea to start with small values and make sure that nothing breaks on your wiki before moving to longer values.",
"admin.security.jwt":"JWT Configuration",
"admin.security.jwtAudience":"JWT Audience",
"admin.security.jwtAudienceHint":"Audience URN used in JWT issued upon login. Usually your domain name. (e.g. urn:your.domain.com)",
"admin.security.loadFailed":"Failed to load the security configuration.",
"admin.security.loginScreen":"Login Screen",
"admin.security.maxUploadBatch":"Max Files per Upload",
"admin.security.trustProxyHint":"Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.",
"admin.security.uploads":"Uploads",
@ -1236,23 +1231,46 @@
"admin.utilities.contentSubtitle":"Various tools for pages",
"admin.utilities.disconnectWSHint":"Force all active websocket connections to be closed.",
"admin.utilities.disconnectWSConfirm":"Every open editing session and admin terminal will be disconnected, on every instance. Their clients reconnect on their own, and unsaved text is not lost.",
"admin.utilities.disconnectWSFailed":"Failed to disconnect the websocket connections.",
"admin.utilities.disconnectWSHint":"Force all active websocket connections to be closed, on every instance.",
"admin.utilities.disconnectWSSuccess":"All active websocket connections have been terminated.",
"admin.utilities.export":"Export",
"admin.utilities.exportHint":"Export content to tarball for backup / migration.",
"admin.utilities.flushCache":"Flush Cache",
"admin.utilities.flushCacheHint":"Pages and Assets are cached to disk for better performance. You can flush the cache to force all content to be fetched from the DB again.",
"admin.utilities.flushCacheFailed":"Failed to flush the cache.",
"admin.utilities.flushCacheHint":"Files, icons and site settings are cached for better performance. Flushing forces everything to be fetched from the database again, on every instance.",
"admin.utilities.flushCacheSuccess":"The cache has been flushed.",
"admin.utilities.graphEndpointSubtitle":"Change the GraphQL endpoint for Wiki.js",
"admin.utilities.invalidAuthCertificatesHint":"Regenerate the public and private keys used for authentication. This will instantly log everyone out.",
"admin.utilities.invalidApiCertificates":"Invalidate API Keys Certificates",
"admin.utilities.invalidApiCertificatesConfirm":"A new passphrase and keypair will be generated, and every API key ever issued will stop working immediately.",
"admin.utilities.invalidApiCertificatesConfirmWarn":"Keys are not deleted — they stay listed, and each one has to be reissued to the integration using it. Nobody is logged out: user sessions are signed with a separate secret.",
"admin.utilities.invalidApiCertificatesFailed":"Failed to regenerate the API keys certificates.",
"admin.utilities.invalidApiCertificatesHint":"Regenerate the passphrase and the keypair API keys are signed with. Every key already issued stops working and must be reissued.",
"admin.utilities.invalidApiCertificatesSuccess":"Certificates regenerated. No API key was still in use. | Certificates regenerated. 1 API key must be reissued. | Certificates regenerated. {count} API keys must be reissued.",
"admin.utilities.invalidSessionSecret":"Invalidate User Sessions Secret",
"admin.utilities.invalidSessionSecretConfirm":"A new secret will be generated for signing session cookies, and every open session will be ended.",
"admin.utilities.invalidSessionSecretConfirmWarn":"Everyone is logged out immediately, you included — you will have to sign in again. The new secret is only used for signing once each server has been restarted. API keys are unaffected.",
"admin.utilities.invalidSessionSecretFailed":"Failed to rotate the user sessions secret.",
"admin.utilities.invalidSessionSecretHint":"Rotate the secret used to sign session cookies and end every open session. Everyone is logged out.",
"admin.utilities.purgeHistory":"Purge History",
"admin.utilities.purgeHistoryConfirm":"Every page version older than **{timeframe}** will be deleted, on every site.",
"admin.utilities.purgeHistoryConfirmWarn":"Pages keep the content they have now, but a discarded version cannot be brought back. Any deleted page older than the selected timeframe cannot be recovered.",
"admin.utilities.purgeHistoryFailed":"Failed to purge the page history.",
"admin.utilities.purgeHistoryHint":"Delete history (content versioning) older than the selected timeframe.",
"admin.utilities.purgeHistorySuccess":"No page version was old enough to purge. | 1 page version purged. | {count} page versions purged.",
"admin.utilities.purgeRevokedKeys":"Purge Revoked API Keys",
"admin.utilities.purgeRevokedKeysConfirm":"Every API key that has been revoked will be deleted permanently.",
"admin.utilities.purgeRevokedKeysConfirmWarn":"Nobody loses access — a revoked key already grants none — but the record that it ever existed goes with it. Keys marked as invalidated are kept.",
"admin.utilities.purgeRevokedKeysFailed":"Failed to purge the revoked API keys.",
"admin.utilities.purgeRevokedKeysHint":"Permanently delete the API keys that have been revoked. Invalidated keys are kept.",
"admin.utilities.purgeRevokedKeysSuccess":"No revoked API key to purge. | 1 revoked API key deleted. | {count} revoked API keys deleted.",
"admin.utilities.scanPageProblems":"Scan for Page Problems",
"admin.utilities.scanPageProblemsHint":"Scan all pages for invalid, missing or corrupted data.",
"admin.utilities.subtitle":"Maintenance and miscellaneous tools",