mirror of https://github.com/requarks/wiki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
56 lines
2.0 KiB
56 lines
2.0 KiB
import { validate as uuidValidate } from 'uuid'
|
|
import crypto from 'node:crypto'
|
|
import type { FastifyInstance } from 'fastify'
|
|
|
|
/**
|
|
* An avatar changes whenever its owner uploads a new one, and the URL never carries a version — so it
|
|
* is always revalidated, and the ETag turns that into an empty 304 rather than a re-download.
|
|
*/
|
|
const AVATAR_CACHE = 'private, no-cache'
|
|
|
|
/**
|
|
* _user Routes
|
|
*
|
|
* Public, like `_site` and `_icons`: avatars appear next to page authors and in user pickers, so a
|
|
* reader who can see a page can see them. Only what a user chose to upload is served, under a URL that
|
|
* has to be known — nothing here enumerates users.
|
|
*/
|
|
async function routes(app: FastifyInstance) {
|
|
/**
|
|
* USER AVATAR
|
|
*
|
|
* `current` resolves to the logged in user, as it does for a site's own assets — a page showing its
|
|
* own avatar then needs no user ID to build the URL with.
|
|
*/
|
|
app.get<{ Params: { userId: string } }>('/:userId/avatar', async (req, reply) => {
|
|
let userId: string | null = null
|
|
if (req.params.userId === 'current') {
|
|
userId = req.session?.authenticated ? (req.session.user?.id ?? null) : null
|
|
} else if (uuidValidate(req.params.userId)) {
|
|
userId = req.params.userId
|
|
}
|
|
if (!userId) {
|
|
return reply.notFound('User not found')
|
|
}
|
|
|
|
const avatar = await WIKI.models.users.getAvatar(userId)
|
|
if (!avatar) {
|
|
return reply.notFound('This user has no avatar')
|
|
}
|
|
|
|
const etag = `"${crypto.createHash('sha1').update(avatar.data).digest('hex')}"`
|
|
reply.header('ETag', etag)
|
|
reply.header('Cache-Control', AVATAR_CACHE)
|
|
// -> The bytes came from a user, so the browser must take the type at its word rather than looking
|
|
// for something more interesting in them
|
|
reply.header('X-Content-Type-Options', 'nosniff')
|
|
if (req.headers['if-none-match'] === etag) {
|
|
return reply.code(304).send()
|
|
}
|
|
|
|
return reply.type(avatar.mime).send(avatar.data)
|
|
})
|
|
}
|
|
|
|
export default routes
|