@ -280,6 +280,82 @@ async function routes(app: FastifyInstance) {
}
)
/**
*LOGOUT
*/
app.post<{Params:{siteId: string}}>(
'/sites/:siteId/auth/logout',
{
schema:{
summary:'Logout',
description:
"Destroys the current session and answers with where to send the user next: the first of the user's groups that sets a logout redirect, otherwise the site's own setting, otherwise the site root. A request that was not logged in gets the same answer rather than an error, so that a client acting on a session the server has already forgotten still ends up somewhere sensible.",
tags:['Authentication'],
params:{
type:'object',
properties:{
siteId:{
type:'string',
format:'uuid'
}
},
required:['siteId']
},
response:{
200:{
description:'Logged out successfully',
type:'object',
properties:{
ok:{
type:'boolean'
},
redirect:{
type:'string',
description:'A path within this wiki, or an absolute URL if one is configured.'
@ -80,7 +80,7 @@ async function routes(app: FastifyInstance) {
schema:{
summary:'List the events a webhook can subscribe to',
description:
'Only `user:join` and `user:login` are emitted at the moment. Pages, assets, comments and logout are not implemented yet, so a subscription to those is stored but never triggered.',
'Only the `user:*` events are emitted at the moment. Pages, assets and comments are not implemented yet, so a subscription to those is stored but never triggered.',
@ -504,9 +504,9 @@ async function routes(app: FastifyInstance) {
permissions:['manage:system']
},
schema:{
summary:'Install an extension',
summary:'Install or reinstall an extension',
description:
'Only extensions flagged `isInstallable` can be installed from here. None currently are: Git and Pandoc come from the operating system, Sharp and Puppeteer are optional dependencies, so both are installed outside the application and this answers 409 pointing at the documentation.',
'Only extensions flagged `isInstallable` can be installed from here — currently Sharp, which is an npm package. It already ships as an optional dependency, so this is mostly a repair: it refetches the package and the prebuilt binary for this OS and architecture, which is what to reach for when the native binary is missing or does not match the platform. Git and Pandoc come from the operating system and answer 409 pointing at the documentation. Runs npm and can take minutes.',
tags:['System'],
params:{
type:'object',
@ -528,6 +528,11 @@ async function routes(app: FastifyInstance) {
},
message:{
type:'string'
},
restartRequired:{
type:'boolean',
description:
'True when this server already tried and failed to load the module. Node replays a failed module load for the life of the process, so the repaired files cannot be used until the server restarts.'
}
}
}
@ -548,9 +553,26 @@ async function routes(app: FastifyInstance) {
)
}
// -> No extension declares itself installable yet; an installer belongs with the extension
// that needs it, next to its definition
returnreply.notImplemented('Installing this extension is not implemented yet.')
try{
awaitWIKI.models.extensions.install(definition)
}catch(err: any){
// -> The message carries npm's own output, which is the only thing that explains a failure
// like a missing build toolchain. An administrator is the only caller.
returnreply.internalServerError(err.message)
}
// -> A fresh install is usable at once, since nothing has tried to load it yet. Repairing one this
// process already choked on is a different story, and saying so beats leaving an administrator
summary:"Update the logged in user's own profile",
description:
'Updates any subset of the profile fields; omitted ones are left unchanged. Requires the current site to have the `profile` feature enabled. The email cannot be changed here, and neither can any field an administrator owns.',
tags:['Users'],
body:{
$ref:'UserProfileUpdate#'
},
response:{
200:{
description:'Profile updated successfully',
type:'object',
properties:{
ok:{
type:'boolean'
},
message:{
type:'string'
},
profile:{
$ref:'UserProfile#'
}
}
}
}
}
},
async(req,reply)=>{
constuserId=sessionUserId(req)
if(!userId){
returnreply.unauthorized()
}
if(!(awaitisProfileEditable(req))){
returnreply.forbidden('Profile editing is disabled on this site.')
}
// -> A bad time zone would break every date the user sees, and the list of valid zones is only
// known at runtime, so it cannot be expressed as a schema enum
// -> The session carries a copy of the name and the preferences, which `/whoami` serves on
// every page load. Left alone, it would hand back the pre-save values.
req.session.user={
...req.session.user!,
name: profile.name,
timezone: profile.timezone,
dateFormat: profile.dateFormat,
timeFormat: profile.timeFormat,
appearance: profile.appearance,
cvd: profile.cvd
}
return{
ok: true,
message:'Profile updated successfully.',
profile
}
}
)
/**
*UPLOADOWNAVATAR
*/
app.put(
'/profile/avatar',
{
schema:{
summary:"Replace the logged in user's own avatar",
description:`The body is the raw image, not a multipart form — send the file itself with its \`Content-Type\`. At most ${avatarUploadLimit/1024/1024} MB, and it must really be one of the accepted formats: the bytes are checked, not the declared type. Resized to a 180x180 JPEG when the Sharp extension is installed, otherwise stored as uploaded. Requires the current site to have the \`profile\` feature enabled.`,
tags:['Users'],
consumes:[...imageMimeTypes],
response:{
200:{
description:'Avatar uploaded successfully',
type:'object',
properties:{
ok:{
type:'boolean'
},
message:{
type:'string'
}
}
}
}
}
},
async(req,reply)=>{
constuserId=sessionUserId(req)
if(!userId){
returnreply.unauthorized()
}
if(!(awaitisProfileEditable(req))){
returnreply.forbidden('Profile editing is disabled on this site.')
}
constdata=req.body
if(!Buffer.isBuffer(data)||data.length<1){
thrownewCustomError('userAvatarEmpty','No image was sent.')
}
// -> The declared content type got the request this far; what the bytes actually are is what
// decides, since they are what gets stored and served back
if(!detectImageMime(data)){
thrownewCustomError(
'userAvatarInvalidImage',
'Not a PNG, JPEG, WebP or GIF image, whatever the request said it was.'
)
}
awaitWIKI.models.users.setAvatar(userId,data)
// -> The account menu reads `hasAvatar` off the session on every page load
'Leaves the user to be rendered as a placeholder again. Succeeds even if there was no avatar to remove. Requires the current site to have the `profile` feature enabled.',
tags:['Users'],
response:{
200:{
description:'Avatar cleared successfully',
type:'object',
properties:{
ok:{
type:'boolean'
},
message:{
type:'string'
}
}
}
}
}
},
async(req,reply)=>{
constuserId=sessionUserId(req)
if(!userId){
returnreply.unauthorized()
}
if(!(awaitisProfileEditable(req))){
returnreply.forbidden('Profile editing is disabled on this site.')