Compare commits
No commits in common. "main" and "bun-deprecated" have entirely different histories.
main
...
bun-deprec
17
Dockerfile
17
Dockerfile
|
@ -2,6 +2,9 @@
|
||||||
FROM node:lts-alpine AS base
|
FROM node:lts-alpine AS base
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
FROM oven/bun:1-debian AS base2
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
FROM base AS install
|
FROM base AS install
|
||||||
RUN mkdir -p /temp/dev
|
RUN mkdir -p /temp/dev
|
||||||
COPY package.json package-lock.json /temp/dev/
|
COPY package.json package-lock.json /temp/dev/
|
||||||
|
@ -16,14 +19,18 @@ COPY --from=install /temp/prod/node_modules node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS prisma-dev
|
||||||
COPY --from=install /temp/dev/node_modules node_modules
|
COPY --from=install /temp/dev/node_modules node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
# vite build
|
|
||||||
RUN NODE_ENV=production npm run build
|
|
||||||
|
|
||||||
FROM base AS release
|
FROM base2 AS build
|
||||||
|
COPY --from=prisma-dev /usr/src/app/node_modules node_modules
|
||||||
|
COPY . .
|
||||||
|
# vite build
|
||||||
|
RUN NODE_ENV=production bun --bun run build
|
||||||
|
|
||||||
|
FROM base2 AS release
|
||||||
COPY --from=prisma /usr/src/app/prisma prisma
|
COPY --from=prisma /usr/src/app/prisma prisma
|
||||||
COPY --from=prisma /usr/src/app/node_modules node_modules
|
COPY --from=prisma /usr/src/app/node_modules node_modules
|
||||||
COPY --from=build /usr/src/app/build build
|
COPY --from=build /usr/src/app/build build
|
||||||
|
@ -31,4 +38,4 @@ COPY --from=build /usr/src/app/assets assets
|
||||||
COPY --from=build /usr/src/app/package.json .
|
COPY --from=build /usr/src/app/package.json .
|
||||||
|
|
||||||
EXPOSE 3000/tcp
|
EXPOSE 3000/tcp
|
||||||
CMD [ "node", "build" ]
|
CMD [ "bun", "run", "./build/index.js" ]
|
|
@ -1,6 +1,6 @@
|
||||||
# ava
|
# ava
|
||||||
|
|
||||||
ava is a simple avatar server written in TypeScript for Node with SvelteKit and OIDC support.
|
ava is a simple avatar server written in TypeScript for Bun with SvelteKit and OIDC support.
|
||||||
This exists for a few reasons:
|
This exists for a few reasons:
|
||||||
1. I wanted to learn SvelteKit
|
1. I wanted to learn SvelteKit
|
||||||
2. I wanted to try implementing OAuth2/OIDC
|
2. I wanted to try implementing OAuth2/OIDC
|
||||||
|
|
924
package-lock.json
generated
924
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ava-node",
|
"name": "ava",
|
||||||
"version": "1.3.2",
|
"version": "1.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
@ -12,13 +12,13 @@
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.2.0",
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@types/node": "^20.14.10",
|
"@types/bun": "^1.1.6",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"prettier-plugin-svelte": "^3.1.2",
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
"prisma": "^5.16.2",
|
"prisma": "^5.16.2",
|
||||||
"svelte": "^4.2.7",
|
"svelte": "^4.2.7",
|
||||||
|
"svelte-adapter-bun": "^0.5.2",
|
||||||
"svelte-check": "^3.6.0",
|
"svelte-check": "^3.6.0",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
|
@ -29,9 +29,9 @@
|
||||||
"@fontsource-variable/inter": "^5.0.18",
|
"@fontsource-variable/inter": "^5.0.18",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||||
"@prisma/client": "5.16.2",
|
"@prisma/client": "5.16.2",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
"sharp": "^0.33.4"
|
"sharp": "^0.33.4",
|
||||||
|
"@sveltejs/kit": "^2.0.0"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@prisma/client",
|
"@prisma/client",
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "Avatar" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"userId" TEXT NOT NULL,
|
|
||||||
CONSTRAINT "Avatar_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "Avatar_id_key" ON "Avatar"("id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "Avatar_userId_key" ON "Avatar"("userId");
|
|
|
@ -1,3 +0,0 @@
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Avatar" ADD COLUMN "altText" TEXT;
|
|
||||||
ALTER TABLE "Avatar" ADD COLUMN "source" TEXT;
|
|
|
@ -1,2 +0,0 @@
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "User" ADD COLUMN "name" TEXT;
|
|
|
@ -21,14 +21,4 @@ model Token {
|
||||||
model User {
|
model User {
|
||||||
userId String @id @unique
|
userId String @id @unique
|
||||||
identifier String
|
identifier String
|
||||||
name String?
|
|
||||||
avatar Avatar?
|
|
||||||
}
|
|
||||||
|
|
||||||
model Avatar {
|
|
||||||
id String @id @unique @default(uuid())
|
|
||||||
user User @relation(fields: [userId], references: [userId])
|
|
||||||
userId String @unique
|
|
||||||
altText String?
|
|
||||||
source String?
|
|
||||||
}
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"
|
import { mkdir, readdir, rm } from "node:fs/promises"
|
||||||
import { existsSync } from "node:fs"
|
import { existsSync } from "node:fs"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import { prisma } from "./clientsingleton"
|
import { prisma } from "./clientsingleton"
|
||||||
import configuration from "./configuration"
|
import configuration from "./configuration"
|
||||||
import Sharp, { type FormatEnum } from "sharp"
|
import Sharp, { type FormatEnum } from "sharp"
|
||||||
import type { Avatar } from "@prisma/client"
|
|
||||||
|
|
||||||
// todo: make customizable
|
// todo: make customizable
|
||||||
export const avatarDirectory = "./.data/avatars"
|
export const avatarDirectory = "./.data/avatars"
|
||||||
|
@ -44,7 +43,7 @@ export function generateMissingAvatar(path: string, size: number, fmt?: keyof Sh
|
||||||
[0][1]
|
[0][1]
|
||||||
)
|
)
|
||||||
|
|
||||||
const buf = await readFile(pathToBestQualityImg)
|
const buf = await Bun.file(pathToBestQualityImg).arrayBuffer()
|
||||||
res(writeAvatar(path, await renderAvatar(buf, size, fmt)))
|
res(writeAvatar(path, await renderAvatar(buf, size, fmt)))
|
||||||
missingAvatarQueue.delete(qid)
|
missingAvatarQueue.delete(qid)
|
||||||
})
|
})
|
||||||
|
@ -105,42 +104,6 @@ export async function getPathToAvatarForIdentifier(identifier: string, size: num
|
||||||
return getPathToAvatarForUid(user?.userId, size, fmt)
|
return getPathToAvatarForUid(user?.userId, size, fmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeAvatar(avatar: Avatar | null) {
|
|
||||||
return avatar
|
|
||||||
? {
|
|
||||||
altText: avatar.altText || "",
|
|
||||||
source: avatar.source || "",
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
altText: "Default profile picture",
|
|
||||||
source: "https://git.sucks.win/split/ava",
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMetadataForIdentifier(identifier: string) {
|
|
||||||
let avatar = await prisma.avatar.findFirst({
|
|
||||||
where: {
|
|
||||||
user: {
|
|
||||||
identifier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return sanitizeAvatar(avatar)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMetadataForUserId(userId: string) {
|
|
||||||
let avatar = await prisma.avatar.findFirst({
|
|
||||||
where: {
|
|
||||||
userId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return sanitizeAvatar(avatar)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Render an avatar at the specified size and format
|
* @description Render an avatar at the specified size and format
|
||||||
* @param bin Image to rerender
|
* @param bin Image to rerender
|
||||||
|
@ -163,7 +126,7 @@ export async function renderAvatar(bin: ArrayBuffer|Buffer, squareSize: number,
|
||||||
if (format) img.toFormat(format)
|
if (format) img.toFormat(format)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
img,
|
buf: await img.toBuffer(),
|
||||||
extension: format || metadata.format,
|
extension: format || metadata.format,
|
||||||
requestedFormat: format,
|
requestedFormat: format,
|
||||||
squareSize,
|
squareSize,
|
||||||
|
@ -188,9 +151,9 @@ export async function writeAvatar(avatarDir: string, renderedAvatar: Awaited<Ret
|
||||||
`${renderedAvatar.squareSize}.${renderedAvatar.extension}`
|
`${renderedAvatar.squareSize}.${renderedAvatar.extension}`
|
||||||
)
|
)
|
||||||
|
|
||||||
await writeFile(
|
await Bun.write(
|
||||||
targetPath,
|
targetPath,
|
||||||
renderedAvatar.img
|
renderedAvatar.buf
|
||||||
)
|
)
|
||||||
|
|
||||||
return targetPath
|
return targetPath
|
||||||
|
@ -200,14 +163,9 @@ export async function setNewAvatar(uid: string, avatar?: File) {
|
||||||
if (uid?.includes("/"))
|
if (uid?.includes("/"))
|
||||||
throw Error("UID cannot include /")
|
throw Error("UID cannot include /")
|
||||||
|
|
||||||
// Delete current avatar directory and avatar database entry
|
// Delete current avatar directory
|
||||||
const userAvatarDirectory = join(avatarDirectory, uid)
|
const userAvatarDirectory = join(avatarDirectory, uid)
|
||||||
await rm(userAvatarDirectory, { recursive: true, force: true })
|
await rm(userAvatarDirectory, { recursive: true, force: true })
|
||||||
await prisma.avatar.deleteMany({
|
|
||||||
where: {
|
|
||||||
userId: uid
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!avatar) return {} // we don't need to set a new one
|
if (!avatar) return {} // we don't need to set a new one
|
||||||
|
|
||||||
|
@ -236,12 +194,5 @@ export async function setNewAvatar(uid: string, avatar?: File) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create new Avatar database entry
|
|
||||||
await prisma.avatar.create({
|
|
||||||
data: {
|
|
||||||
userId: uid
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return time
|
return time
|
||||||
}
|
}
|
|
@ -1,32 +1,38 @@
|
||||||
import Sharp, { type FormatEnum } from 'sharp';
|
import Sharp, { type FormatEnum } from "sharp"
|
||||||
import { env } from '$env/dynamic/private';
|
|
||||||
const configuration = {
|
const configuration = {
|
||||||
oauth2: {
|
oauth2: {
|
||||||
endpoints: {
|
endpoints: {
|
||||||
authenticate: env.OAUTH2__AUTHENTICATE!,
|
authenticate: process.env.OAUTH2__AUTHENTICATE!,
|
||||||
logout: env.OAUTH2__LOGOUT,
|
logout: process.env.OAUTH2__LOGOUT,
|
||||||
token: env.OAUTH2__GET_TOKEN!
|
token: process.env.OAUTH2__GET_TOKEN!
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
id: env.OAUTH2__CLIENT_ID!,
|
id: process.env.OAUTH2__CLIENT_ID!,
|
||||||
secret: env.OAUTH2__CLIENT_SECRET!,
|
secret: process.env.OAUTH2__CLIENT_SECRET!,
|
||||||
scopes: env.OAUTH2__SCOPES!
|
scopes: process.env.OAUTH2__SCOPES!
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userinfo: {
|
userinfo: {
|
||||||
route: env.USERINFO__ROUTE!,
|
route: process.env.USERINFO__ROUTE!,
|
||||||
identifier: env.USERINFO__IDENTIFIER!
|
identifier: process.env.USERINFO__IDENTIFIER!
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
permitted_input: (env.ALLOWED_TYPES || env.IMAGES__ALLOWED_INPUT_FORMATS)?.split(',') || [],
|
permitted_input:
|
||||||
default_resolution: parseInt((env.IMAGES__DEFAULT_RESOLUTION || '').toString(), 10) || 512,
|
(
|
||||||
extra_output_types:
|
process.env.ALLOWED_TYPES
|
||||||
(env.IMAGES__EXTRA_OUTPUT_FORMATS?.split(',').filter(
|
|| process.env.IMAGES__ALLOWED_INPUT_FORMATS
|
||||||
(e) => e in Sharp.format
|
)?.split(",") || [],
|
||||||
) as (keyof FormatEnum)[]) || [],
|
default_resolution: parseInt((process.env.IMAGES__DEFAULT_RESOLUTION || "").toString(), 10) || 512,
|
||||||
output_resolutions: env.IMAGES__OUTPUT_RESOLUTIONS?.split(',').map((e) => parseInt(e, 10)) || [
|
extra_output_types:
|
||||||
1024, 512, 256, 128, 64, 32
|
process.env.IMAGES__EXTRA_OUTPUT_FORMATS
|
||||||
]
|
?.split(",")
|
||||||
}
|
.filter(e => e in Sharp.format) as (keyof FormatEnum)[]
|
||||||
};
|
|| [],
|
||||||
export default configuration;
|
output_resolutions:
|
||||||
|
process.env.IMAGES__OUTPUT_RESOLUTIONS
|
||||||
|
?.split(",")
|
||||||
|
.map(e => parseInt(e,10))
|
||||||
|
|| [ 1024, 512, 256, 128, 64, 32 ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default configuration
|
|
@ -126,13 +126,11 @@ export async function getUserInfo(id: string) {
|
||||||
userId: userInfo.sub,
|
userId: userInfo.sub,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
identifier: userInfo[configuration.userinfo.identifier],
|
identifier: userInfo[configuration.userinfo.identifier]
|
||||||
name: userInfo.name
|
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
userId: userInfo.sub,
|
userId: userInfo.sub,
|
||||||
identifier: userInfo[configuration.userinfo.identifier],
|
identifier: userInfo[configuration.userinfo.identifier]
|
||||||
name: userInfo.name
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { getPathToAvatarForIdentifier } from '$lib/avatars.js';
|
import { getPathToAvatarForIdentifier } from '$lib/avatars.js';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { readFile } from 'fs/promises';
|
|
||||||
import mime from "mime";
|
|
||||||
|
|
||||||
export async function GET({ params : { identifier, size }, url }) {
|
export async function GET({ params : { identifier, size }, url }) {
|
||||||
let sz = size ? parseInt(size, 10) : undefined
|
let sz = size ? parseInt(size, 10) : undefined
|
||||||
|
@ -13,9 +11,5 @@ export async function GET({ params : { identifier, size }, url }) {
|
||||||
if (!avPath)
|
if (!avPath)
|
||||||
throw error(404, "Avatar at this size not found, or this is an invalid format")
|
throw error(404, "Avatar at this size not found, or this is an invalid format")
|
||||||
|
|
||||||
return new Response(await readFile(avPath), {
|
return new Response(Bun.file(avPath))
|
||||||
headers: {
|
|
||||||
"Content-Type": mime.getType(avPath) || ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
import { getMetadataForIdentifier } from '$lib/avatars.js';
|
|
||||||
import { prisma } from '$lib/clientsingleton.js';
|
|
||||||
import { error, redirect } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export async function load({ params : { identifier } }) {
|
|
||||||
let userInfo = await prisma.user.findFirst({ where: { identifier } })
|
|
||||||
if (!userInfo)
|
|
||||||
throw error(404, "User not found")
|
|
||||||
let metadata = await getMetadataForIdentifier(identifier)
|
|
||||||
return {
|
|
||||||
name: userInfo.name || identifier,
|
|
||||||
identifier,
|
|
||||||
...metadata
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import FilePreviewSet from "../../set/FilePreviewSet.svelte";
|
|
||||||
|
|
||||||
export let data: {
|
|
||||||
identifier: string,
|
|
||||||
name: string,
|
|
||||||
altText: string,
|
|
||||||
source: string
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
h5, p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--link)
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<h1>{data.name}'s avatar</h1>
|
|
||||||
<FilePreviewSet avatarUrl="/avatar/{data.identifier}" style="border-radius:8px;" />
|
|
||||||
<br>
|
|
||||||
<FilePreviewSet avatarUrl="/avatar/{data.identifier}" style="border-radius:100%;" />
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<h5>Alt text</h5>
|
|
||||||
<p>
|
|
||||||
{#if data.altText}
|
|
||||||
{data.altText}
|
|
||||||
{:else}
|
|
||||||
<em>No alt text available</em>
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<h5>Source</h5>
|
|
||||||
<p>
|
|
||||||
{#if data.source}
|
|
||||||
{data.source}
|
|
||||||
{:else}
|
|
||||||
<em>No source available</em>
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { getMetadataForIdentifier } from '$lib/avatars.js';
|
|
||||||
import { error, redirect } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export async function GET({ params : { identifier } }) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify(
|
|
||||||
await getMetadataForIdentifier(identifier)
|
|
||||||
),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Access-Control-Allow-Origin": "*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { getMetadataForIdentifier } from '$lib/avatars.js';
|
|
||||||
import { error, redirect } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export async function GET({ params : { identifier } }) {
|
|
||||||
let { source } = await getMetadataForIdentifier(identifier)
|
|
||||||
let finalUrl: URL | string = `/info/${identifier}`
|
|
||||||
|
|
||||||
if (source) {
|
|
||||||
try {
|
|
||||||
finalUrl = new URL(source)
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw redirect(302, finalUrl)
|
|
||||||
}
|
|
|
@ -1,17 +1,15 @@
|
||||||
import {getRequestUser, launchLogin} from "$lib/oidc"
|
import {getRequestUser, launchLogin} from "$lib/oidc"
|
||||||
import configuration from "$lib/configuration.js";
|
import configuration from "$lib/configuration.js";
|
||||||
import { fail } from "@sveltejs/kit";
|
import { fail } from "@sveltejs/kit";
|
||||||
import { avatarDirectory, getMetadataForUserId, setNewAvatar } from "$lib/avatars.js";
|
import { avatarDirectory, setNewAvatar } from "$lib/avatars.js";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { prisma } from "$lib/clientsingleton";
|
|
||||||
export async function load({ request, parent, url }) {
|
export async function load({ request, parent, url }) {
|
||||||
const { user } = await parent();
|
const { user } = await parent();
|
||||||
if (!user)
|
if (!user)
|
||||||
return launchLogin(url.toString())
|
launchLogin(url.toString())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: url.toString(),
|
url: url.toString(),
|
||||||
avatar: await getMetadataForUserId(user.sub),
|
|
||||||
allowedImageTypes: configuration.images.permitted_input,
|
allowedImageTypes: configuration.images.permitted_input,
|
||||||
renderSizes: configuration.images.output_resolutions
|
renderSizes: configuration.images.output_resolutions
|
||||||
}
|
}
|
||||||
|
@ -23,33 +21,16 @@ export const actions = {
|
||||||
if (!user)
|
if (!user)
|
||||||
return fail(401, {error: "unauthenticated"})
|
return fail(401, {error: "unauthenticated"})
|
||||||
|
|
||||||
let submission = Object.fromEntries((await request.formData()).entries());
|
let submission = await request.formData();
|
||||||
let newAvatar = submission.newAvatar
|
let newAvatar = undefined
|
||||||
let timing: Awaited<ReturnType<typeof setNewAvatar>> = {}
|
if (submission.get("action") != "Clear") {
|
||||||
let isUploadingNewFile = submission.action == "Clear"
|
newAvatar = submission.get("newAvatar")
|
||||||
|
if (newAvatar !== undefined && !(newAvatar instanceof File))
|
||||||
if (
|
return fail(400, {success: false, error: "incorrect entry type"})
|
||||||
!isUploadingNewFile // if action isn't already clear
|
|
||||||
&& newAvatar !== undefined // and avatar is defined
|
|
||||||
&& (newAvatar instanceof File && newAvatar.size > 0)
|
|
||||||
) {
|
|
||||||
if (!configuration.images.permitted_input.includes(newAvatar.type))
|
if (!configuration.images.permitted_input.includes(newAvatar.type))
|
||||||
return fail(400, {success: false, error: `allowed types does not include ${newAvatar.type}`})
|
return fail(400, {success: false, error: `allowed types does not include ${newAvatar.type}`})
|
||||||
isUploadingNewFile = true
|
|
||||||
}
|
}
|
||||||
|
let timing = await setNewAvatar(user.sub, newAvatar)
|
||||||
if (isUploadingNewFile)
|
|
||||||
timing = await setNewAvatar(user.sub, newAvatar as File|null || undefined)
|
|
||||||
if (await prisma.avatar.findFirst({ where: { userId: user.sub } }))
|
|
||||||
await prisma.avatar.update({
|
|
||||||
where: {
|
|
||||||
userId: user.sub
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
altText: typeof submission.altText == "string" ? submission.altText : null,
|
|
||||||
source: typeof submission.source == "string" ? submission.source : null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
@ -2,66 +2,37 @@
|
||||||
import type { User } from "$lib/types";
|
import type { User } from "$lib/types";
|
||||||
import FilePreviewSet from "./FilePreviewSet.svelte";
|
import FilePreviewSet from "./FilePreviewSet.svelte";
|
||||||
|
|
||||||
export let data: {
|
export let data: {user: User, url: string, allowedImageTypes: string[], renderSizes: number[]};
|
||||||
user: User,
|
|
||||||
url: string,
|
|
||||||
avatar: {
|
|
||||||
altText: string,
|
|
||||||
source: string,
|
|
||||||
default: boolean
|
|
||||||
}
|
|
||||||
allowedImageTypes: string[],
|
|
||||||
renderSizes: number[]
|
|
||||||
};
|
|
||||||
export let form: { success: true, message: string } | { success: false, error: string } | undefined;
|
export let form: { success: true, message: string } | { success: false, error: string } | undefined;
|
||||||
let files: FileList;
|
let files: FileList;
|
||||||
let fileSrc = `/avatar/${data.user.identifier}/`
|
let fileSrc = `/avatar/${data.user.identifier}/`
|
||||||
|
|
||||||
$: if (files && files.length >= 0) {
|
$: if (files && files.length >= 0) {
|
||||||
data.avatar.altText = "", data.avatar.source = "", data.avatar.default = false
|
console.log(files.length)
|
||||||
fileSrc = URL.createObjectURL(files.item(0)!)
|
fileSrc = URL.createObjectURL(files.item(0)!)
|
||||||
} else fileSrc = `/avatar/${data.user.identifier}/`
|
} else fileSrc = `/avatar/${data.user.identifier}/`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
form {
|
form {
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
form, form > .buttons, form > .metadata {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
form > .metadata {
|
form > * {
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
form > .metadata > textarea {
|
|
||||||
height: 3em;
|
|
||||||
flex-grow: 1;
|
|
||||||
min-width: 15em;
|
|
||||||
}
|
|
||||||
form > .buttons {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
form input {
|
|
||||||
font-family: "Inter Variable", "Inter", sans-serif;
|
font-family: "Inter Variable", "Inter", sans-serif;
|
||||||
}
|
}
|
||||||
form > input[type="file"] {
|
form > input[type="file"] {
|
||||||
flex-basis: 100%;
|
width: 100%;
|
||||||
min-height: 1em;
|
|
||||||
}
|
}
|
||||||
form input[type="submit"], form input[type="file"] {
|
form > input[type="submit"], form > input[type="file"] {
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
form input[type="submit"], form input[type="file"], form textarea {
|
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
|
cursor: pointer;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--link);
|
border: 1px solid var(--link);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background-color: var(--crust);
|
background-color: var(--crust);
|
||||||
}
|
}
|
||||||
form textarea:disabled {
|
|
||||||
color: var(--link);
|
|
||||||
}
|
|
||||||
form > input[type="file"]::file-selector-button {
|
form > input[type="file"]::file-selector-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -102,19 +73,13 @@
|
||||||
</p>
|
</p>
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<input type="file" bind:files={files} accept={data.allowedImageTypes.join(",")} name="newAvatar">
|
<input type="file" bind:files={files} accept={data.allowedImageTypes.join(",")} name="newAvatar">
|
||||||
<div class="metadata">
|
<input type="submit" name="action" value="Upload">
|
||||||
<textarea name="altText" placeholder="Describe your image" disabled={data.avatar.default}>{data.avatar.altText}</textarea>
|
<input type="submit" name="action" value="Clear">
|
||||||
<textarea name="source" placeholder="Provide a source for your image" disabled={data.avatar.default}>{data.avatar.source}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="buttons">
|
|
||||||
<input type="submit" name="action" value="Save">
|
|
||||||
<input type="submit" name="action" value="Clear">
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
{#if form}
|
{#if form}
|
||||||
{#if form.success}
|
{#if form.success}
|
||||||
<details>
|
<details>
|
||||||
<summary><small>Avatar updated successfully</small></summary>
|
<summary><small>Avatar set successfully</small></summary>
|
||||||
<div>
|
<div>
|
||||||
<pre>{form.message}</pre>
|
<pre>{form.message}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import adapter from '@sveltejs/adapter-node';
|
import adapter from 'svelte-adapter-bun';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
|
Loading…
Reference in a new issue