diff --git a/bun.lockb b/bun.lockb index 9df74ce..3e6971a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/imagemagick.policy.xml b/imagemagick.policy.xml new file mode 100644 index 0000000..34f5341 --- /dev/null +++ b/imagemagick.policy.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index fa40232..ee2b869 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/lib/avatars.ts b/src/lib/avatars.ts index 6357bdb..1e62eb4 100644 --- a/src/lib/avatars.ts +++ b/src/lib/avatars.ts @@ -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 = {} + + // 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 } \ No newline at end of file diff --git a/src/lib/oidc.ts b/src/lib/oidc.ts index 32c7612..8bab640 100644 --- a/src/lib/oidc.ts +++ b/src/lib/oidc.ts @@ -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, @@ -121,6 +121,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) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a1c0c36..548890f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -50,7 +50,7 @@ nav > * { display: flex; /* Flexbox fixes everything! */ } - a { + a, small { color: var(--link) } code, pre { diff --git a/src/routes/set/+page.server.ts b/src/routes/set/+page.server.ts index c750195..f01e9c2 100644 --- a/src/routes/set/+page.server.ts +++ b/src/routes/set/+page.server.ts @@ -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") + } } } \ No newline at end of file diff --git a/src/routes/set/+page.svelte b/src/routes/set/+page.svelte index d9808e9..845a132 100644 --- a/src/routes/set/+page.svelte +++ b/src/routes/set/+page.svelte @@ -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 @@ Avatar URLs...

-
+
+{#if form} + {#if form.success} +
+ Avatar set successfully +
+
{form.message}
+
+
+ {:else} + An error occurred: {form.error} + {/if} +{/if} {#key fileSrc}
diff --git a/static/default/1024.png b/static/default/1024.png new file mode 100644 index 0000000..cee6e06 Binary files /dev/null and b/static/default/1024.png differ