add profile picture set
This commit is contained in:
parent
6088659cdb
commit
55e0e1f91e
134
imagemagick.policy.xml
Normal file
134
imagemagick.policy.xml
Normal file
|
@ -0,0 +1,134 @@
|
|||
<!--
|
||||
|
||||
Creating a security policy that fits your specific local environment
|
||||
before making use of ImageMagick is highly advised. You can find guidance on
|
||||
setting up this policy at https://imagemagick.org/script/security-policy.php,
|
||||
and it's important to verify your policy using the validation tool located
|
||||
at https://imagemagick-secevaluator.doyensec.com/.
|
||||
|
||||
|
||||
Web-safe ImageMagick security policy:
|
||||
|
||||
This security protocol designed for web-safe usage focuses on situations
|
||||
where ImageMagick is applied in publicly accessible contexts, like websites.
|
||||
It deactivates the capability to read from or write to any image formats
|
||||
other than web-safe formats like GIF, JPEG, and PNG. Additionally, this
|
||||
policy prohibits the execution of image filters and indirect reads, thereby
|
||||
thwarting potential security breaches. By implementing these limitations,
|
||||
the web-safe policy fortifies the safeguarding of systems accessible to
|
||||
the public, reducing the risk of exploiting ImageMagick's capabilities
|
||||
for potential attacks.
|
||||
-->
|
||||
<policymap>
|
||||
<!-- Set maximum parallel threads. -->
|
||||
<policy domain="resource" name="thread" value="2"/>
|
||||
<!--
|
||||
Set maximum time to live in seconds or mnemonics, e.g. "2 minutes". When
|
||||
this limit is exceeded, an exception is thrown and processing stops.
|
||||
-->
|
||||
<policy domain="resource" name="time" value="60"/>
|
||||
<!--
|
||||
Set maximum number of open pixel cache files. When this limit is
|
||||
exceeded, any subsequent pixels cached to disk are closed and reopened
|
||||
on demand.
|
||||
-->
|
||||
<policy domain="resource" name="file" value="768"/>
|
||||
<!--
|
||||
Set maximum amount of memory in bytes to allocate for the pixel cache
|
||||
from the heap. When this limit is exceeded, the image pixels are cached
|
||||
to memory-mapped disk.
|
||||
-->
|
||||
<policy domain="resource" name="memory" value="256MiB"/>
|
||||
<!--
|
||||
Set maximum amount of memory map in bytes to allocate for the pixel
|
||||
cache. When this limit is exceeded, the image pixels are cached to
|
||||
disk.
|
||||
-->
|
||||
<policy domain="resource" name="map" value="512MiB"/>
|
||||
<!--
|
||||
Set the maximum width * height of an image that can reside in the pixel
|
||||
cache memory. Images that exceed the area limit are cached to disk.
|
||||
-->
|
||||
<policy domain="resource" name="area" value="16KP"/>
|
||||
<!--
|
||||
Set maximum amount of disk space in bytes permitted for use by the pixel
|
||||
cache. When this limit is exceeded, the pixel cache is not be created
|
||||
and an exception is thrown.
|
||||
-->
|
||||
<policy domain="resource" name="disk" value="1GiB"/>
|
||||
<!--
|
||||
Set the maximum length of an image sequence. When this limit is
|
||||
exceeded, an exception is thrown.
|
||||
-->
|
||||
<policy domain="resource" name="list-length" value="16"/>
|
||||
<!--
|
||||
Set the maximum width of an image. When this limit is exceeded, an
|
||||
exception is thrown.
|
||||
-->
|
||||
<policy domain="resource" name="width" value="4KP"/>
|
||||
<!--
|
||||
Set the maximum height of an image. When this limit is exceeded, an
|
||||
exception is thrown.
|
||||
-->
|
||||
<policy domain="resource" name="height" value="4KP"/>
|
||||
<!--
|
||||
Periodically yield the CPU for at least the time specified in
|
||||
milliseconds.
|
||||
-->
|
||||
<policy domain="resource" name="throttle" value="2"/>
|
||||
<!--
|
||||
Do not create temporary files in the default shared directories, instead
|
||||
specify a private area to store only ImageMagick temporary files.
|
||||
-->
|
||||
<!--
|
||||
<policy domain="resource" name="temporary-path" value="/magick/tmp/"/>
|
||||
-->
|
||||
<!--
|
||||
Force memory initialization by memory mapping select memory
|
||||
allocations.
|
||||
-->
|
||||
<policy domain="cache" name="memory-map" value="anonymous"/>
|
||||
<!--
|
||||
Ensure all image data is fully flushed and synchronized to disk.
|
||||
-->
|
||||
<policy domain="cache" name="synchronize" value="true"/>
|
||||
<!--
|
||||
Replace passphrase for secure distributed processing
|
||||
-->
|
||||
<!--
|
||||
<policy domain="cache" name="shared-secret" value="secret-passphrase" stealth="true"/>
|
||||
-->
|
||||
<!-- Do not permit any delegates to execute. -->
|
||||
<policy domain="delegate" rights="none" pattern="*"/>
|
||||
<!-- Do not permit any image filters to load. -->
|
||||
<policy domain="filter" rights="none" pattern="*"/>
|
||||
<!-- Don't read/write from/to stdin/stdout. -->
|
||||
<policy domain="path" rights="none" pattern="-"/>
|
||||
<!-- don't read sensitive paths. -->
|
||||
<policy domain="path" rights="none" pattern="/etc/*"/>
|
||||
<!-- Indirect reads are not permitted. -->
|
||||
<policy domain="path" rights="none" pattern="@*"/>
|
||||
<!--
|
||||
Deny all image modules and specifically exempt reading or writing
|
||||
web-safe image formats.
|
||||
-->
|
||||
<policy domain="module" rights="none" pattern="*"/>
|
||||
<policy domain="module" rights="read | write" pattern="{BMP,GIF,JPEG,PNG,TIFF,WEBP}"/>
|
||||
<policy domain="module" rights="read | write" pattern="{MPC}" stealth="true"/>
|
||||
<policy domain="module" rights="write" pattern="{JSON,INFO,PNM,PS}"/>
|
||||
<!--
|
||||
This policy sets the number of times to replace content of certain
|
||||
memory buffers and temporary files before they are freed or deleted.
|
||||
-->
|
||||
<policy domain="system" name="shred" value="1"/>
|
||||
<!--
|
||||
Enable the initialization of buffers with zeros, resulting in a minor
|
||||
performance penalty but with improved security.
|
||||
-->
|
||||
<policy domain="system" name="memory-map" value="anonymous"/>
|
||||
<!--
|
||||
Set the maximum amount of memory in bytes that are permitted for
|
||||
allocation requests.
|
||||
-->
|
||||
<policy domain="system" name="max-memory-request" value="256MiB"/>
|
||||
</policymap>
|
|
@ -30,7 +30,8 @@
|
|||
"@fontsource-variable/inter": "^5.0.18",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||
"@prisma/client": "5.16.2",
|
||||
"magickwand.js": "^1.1.0"
|
||||
"magickwand.js": "^1.1.0",
|
||||
"mime": "^4.0.4"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"magickwand.js"
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { readdir } from "node:fs/promises"
|
||||
import { mkdir, readdir, rm } from "node:fs/promises"
|
||||
import { existsSync } from "node:fs"
|
||||
import { basename, join } from "node:path"
|
||||
import { join } from "node:path"
|
||||
import { prisma } from "./clientsingleton"
|
||||
import configuration from "./configuration"
|
||||
import { Magick, MagickCore } from "magickwand.js"
|
||||
import mime from "mime"
|
||||
|
||||
Magick.SetSecurityPolicy(await Bun.file('./imagemagick.policy.xml').text())
|
||||
|
||||
// todo: make customizable
|
||||
export const avatarDirectory = "./.data/avatars"
|
||||
export const defaultAvatarDirectory = "./static/default/"
|
||||
export const renderSizes = [ 512, 256, 128, 64, 32 ]
|
||||
export const renderSizes = [ 1024, 512, 256, 128, 64, 32 ]
|
||||
|
||||
export async function getPathToAvatarForUid(uid?: string, size: string = "index") {
|
||||
export async function getPathToAvatarForUid(uid?: string, size: string = "512") {
|
||||
if (uid?.includes("/"))
|
||||
throw Error("UID cannot include /")
|
||||
|
||||
|
@ -22,12 +27,68 @@ export async function getPathToAvatarForUid(uid?: string, size: string = "index"
|
|||
return join(userAvatarDirectory, targetAvatar)
|
||||
}
|
||||
|
||||
export async function getPathToAvatarForIdentifier(identifier: string, size: string = "index") {
|
||||
export async function getPathToAvatarForIdentifier(identifier: string, size: string = "512") {
|
||||
let user = await prisma.user.findFirst({
|
||||
where: {
|
||||
identifier
|
||||
}
|
||||
})
|
||||
|
||||
return getPathToAvatarForUid(user?.userId || "", size)
|
||||
return getPathToAvatarForUid(user?.userId, size)
|
||||
}
|
||||
|
||||
export async function rerenderAvatar(bin: ArrayBuffer, squareSize?: number) {
|
||||
let img = new Magick.Image;
|
||||
// read file
|
||||
await img.readAsync(new Magick.Blob(bin),"")
|
||||
if (squareSize) {
|
||||
// resize, but don't upscale, while filling without squishing
|
||||
await img.scaleAsync(`${squareSize}x${squareSize}^>`)
|
||||
}
|
||||
// center crop
|
||||
const size = img.size()
|
||||
squareSize = Math.min(size.width(), size.height())
|
||||
await img.extentAsync(`${squareSize}x${squareSize}`, MagickCore.CenterGravity)
|
||||
|
||||
// return avatar buffer
|
||||
let tempBlob = new Magick.Blob()
|
||||
await img.writeAsync(tempBlob)
|
||||
return tempBlob.dataAsync()
|
||||
}
|
||||
|
||||
export async function setNewAvatar(uid: string, avatar?: File) {
|
||||
if (uid?.includes("/"))
|
||||
throw Error("UID cannot include /")
|
||||
|
||||
// Delete current avatar directory
|
||||
const userAvatarDirectory = join(avatarDirectory, uid)
|
||||
await rm(userAvatarDirectory, { recursive: true, force: true })
|
||||
|
||||
if (!avatar) return {} // we don't need to set a new one
|
||||
|
||||
// make a new directory
|
||||
mkdir(userAvatarDirectory, { recursive: true })
|
||||
|
||||
let time: Record<number, number> = {}
|
||||
|
||||
// render all images and write to disk
|
||||
let avatarData = await avatar.arrayBuffer()
|
||||
let fileExtension = mime.getExtension(avatar.type)
|
||||
for (let x of renderSizes) {
|
||||
console.log(x)
|
||||
try {
|
||||
let start = Date.now()
|
||||
let rerenderedAvatarData = await rerenderAvatar(avatarData, x)
|
||||
await Bun.write(
|
||||
join(userAvatarDirectory, `${x}.${fileExtension}`),
|
||||
rerenderedAvatarData
|
||||
)
|
||||
time[x] = Date.now()-start
|
||||
} catch (e) { // clear pfp and throw if error encountered
|
||||
await rm(userAvatarDirectory, { recursive: true, force: true })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return time
|
||||
}
|
|
@ -96,7 +96,7 @@ export async function getUserInfo(id: string) {
|
|||
if (userInfoCache.has(tokenInfo.owner))
|
||||
userInfo = userInfoCache.get(tokenInfo.owner)
|
||||
else {
|
||||
let userInfoRequest = await fetchUserInfo(tokenInfo.token)
|
||||
let userInfoRequest = await fetchUserInfo(tokenInfo.token)
|
||||
if (!userInfoRequest.ok) {
|
||||
// assume that token has expired.
|
||||
// try fetching a new one
|
||||
|
@ -108,7 +108,7 @@ export async function getUserInfo(id: string) {
|
|||
})
|
||||
|
||||
if (!token) return // refresh failed. back out
|
||||
prisma.token.update({
|
||||
await prisma.token.update({
|
||||
where: { id },
|
||||
data: {
|
||||
token: token.access_token,
|
||||
|
@ -122,6 +122,21 @@ export async function getUserInfo(id: string) {
|
|||
|
||||
userInfo = await userInfoRequest.json()
|
||||
|
||||
// update user
|
||||
console.log('aaa')
|
||||
await prisma.user.upsert({
|
||||
where: {
|
||||
userId: userInfo.sub,
|
||||
},
|
||||
update: {
|
||||
identifier: userInfo[configuration.userinfo.identifier]
|
||||
},
|
||||
create: {
|
||||
userId: userInfo.sub,
|
||||
identifier: userInfo[configuration.userinfo.identifier]
|
||||
}
|
||||
})
|
||||
|
||||
// cache userinfo
|
||||
userInfoCache.set(tokenInfo.owner, userInfo)
|
||||
setTimeout(() => userInfoCache.delete(tokenInfo.owner), 60*60*1000)
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
nav > * {
|
||||
display: flex; /* Flexbox fixes everything! */
|
||||
}
|
||||
a {
|
||||
a, small {
|
||||
color: var(--link)
|
||||
}
|
||||
code, pre {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import {launchLogin} from "$lib/oidc"
|
||||
import {getRequestUser, launchLogin} from "$lib/oidc"
|
||||
import configuration from "$lib/configuration.js";
|
||||
import { fail } from "@sveltejs/kit";
|
||||
import { avatarDirectory, renderSizes, setNewAvatar } from "$lib/avatars.js";
|
||||
import { join } from "path";
|
||||
export async function load({ request, parent }) {
|
||||
const { user } = await parent();
|
||||
if (!user)
|
||||
|
@ -7,6 +10,31 @@ export async function load({ request, parent }) {
|
|||
|
||||
return {
|
||||
url: request.url,
|
||||
allowedImageTypes: configuration.allowed_types
|
||||
allowedImageTypes: configuration.allowed_types,
|
||||
renderSizes
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
set: async ({request, cookies}) => {
|
||||
let user = await getRequestUser(request, cookies);
|
||||
if (!user)
|
||||
return fail(401, {error: "unauthenticated"})
|
||||
|
||||
let submission = await request.formData();
|
||||
let newAvatar = submission.get("newAvatar")
|
||||
if (newAvatar !== undefined && !(newAvatar instanceof File))
|
||||
return fail(400, {success: false, error: "incorrect entry type"})
|
||||
if (!configuration.allowed_types.includes(newAvatar.type))
|
||||
return fail(400, {success: false, error: `allowed types does not include ${newAvatar.type}`})
|
||||
|
||||
let time = await setNewAvatar(user.sub, newAvatar)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: Object.entries(time)
|
||||
.map(([res, time]) => `${res}x${res} took ${time}ms to render`)
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,8 @@
|
|||
import type { User } from "$lib/types";
|
||||
import FilePreviewSet from "./FilePreviewSet.svelte";
|
||||
|
||||
export let data: {user: User, url: string, allowedImageTypes: string[]};
|
||||
export let data: {user: User, url: string, allowedImageTypes: string[], renderSizes: number[]};
|
||||
export let form: { success: true, message: string } | { success: false, error: string } | undefined;
|
||||
let files: FileList;
|
||||
let fileSrc = `/avatar/${data.user.identifier}/`
|
||||
|
||||
|
@ -66,18 +67,30 @@
|
|||
<summary>Avatar URLs...</summary>
|
||||
<div>
|
||||
<ul>
|
||||
{#each ["", "32", "64", "128", "256", "512"] as variant}
|
||||
{#each ["", ...data.renderSizes] as variant}
|
||||
<li>{new URL(`/avatar/${data.user.identifier}/${variant}`, data.url)}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</p>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<form method="post" enctype="multipart/form-data" action="?/set">
|
||||
<label for="newAvatar">Set a new avatar ➜</label>
|
||||
<input type="file" bind:files={files} accept={data.allowedImageTypes.join(",")} name="newAvatar">
|
||||
<input type="submit" value="Upload">
|
||||
</form>
|
||||
{#if form}
|
||||
{#if form.success}
|
||||
<details>
|
||||
<summary><small>Avatar set successfully</small></summary>
|
||||
<div>
|
||||
<pre>{form.message}</pre>
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<small>An error occurred: {form.error}</small>
|
||||
{/if}
|
||||
{/if}
|
||||
{#key fileSrc}
|
||||
<br>
|
||||
<FilePreviewSet avatarUrl={fileSrc} style="border-radius:8px;" />
|
||||
|
|
BIN
static/default/1024.png
Normal file
BIN
static/default/1024.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
Loading…
Reference in a new issue