Compare commits
No commits in common. "c3e6f481bbf191f0093be291ff2457a0624827ae" and "ae68e5a5cb49fa6c2e714e012a1351794a45b915" have entirely different histories.
c3e6f481bb
...
ae68e5a5cb
15 changed files with 24 additions and 388 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,4 +12,3 @@ node_modules
|
||||||
|
|
||||||
dev.db
|
dev.db
|
||||||
local.db
|
local.db
|
||||||
uploads
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
let canvasElement: HTMLCanvasElement;
|
|
||||||
let errorMessage = $state("");
|
|
||||||
let photoObjectUrl = $state("");
|
|
||||||
|
|
||||||
function handleFileChange(event: Event) {
|
|
||||||
const file = (event.target as HTMLInputElement).files?.[0];
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
errorMessage = `No file selected, try again`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = canvasElement.getContext("2d");
|
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
errorMessage = `Could not get 2d context from canvas`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
|
||||||
|
|
||||||
photoObjectUrl = URL.createObjectURL(file);
|
|
||||||
const image = new Image();
|
|
||||||
image.src = photoObjectUrl;
|
|
||||||
image.onload = () => {
|
|
||||||
const orientation = image.width > image.height ? "landscape" : "portrait";
|
|
||||||
|
|
||||||
if (orientation === "landscape") {
|
|
||||||
ctx.drawImage(image, 0, 0, canvasElement.width, canvasElement.height);
|
|
||||||
} else {
|
|
||||||
ctx.drawImage(image, 0, 0, canvasElement.height, canvasElement.width);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if errorMessage}
|
|
||||||
<div class="error-message">
|
|
||||||
{errorMessage}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="/admin/photos"
|
|
||||||
enctype="multipart/form-data"
|
|
||||||
class="admin-form"
|
|
||||||
>
|
|
||||||
<canvas bind:this={canvasElement}></canvas>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label for="title">Title</label>
|
|
||||||
<input type="text" name="title" id="title" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<input
|
|
||||||
onchange={handleFileChange}
|
|
||||||
type="file"
|
|
||||||
name="file"
|
|
||||||
accept="image/*"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label for="description">Description</label>
|
|
||||||
<textarea name="description" id="description"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit">Upload</button>
|
|
||||||
</form>
|
|
||||||
|
|
@ -1,45 +1,38 @@
|
||||||
import type { Cookies } from "@sveltejs/kit";
|
import type { Cookies } from "@sveltejs/kit";
|
||||||
|
|
||||||
export class CookieAuthentication {
|
export class CookieAuthentication {
|
||||||
|
|
||||||
private readonly cookieValue: string;
|
private readonly cookieValue: string;
|
||||||
private readonly cookieValueArray: string[];
|
private readonly cookieValueArray: string[];
|
||||||
public static cookieName = "auth";
|
public static cookieName = 'auth'
|
||||||
public static adminAuthRole = "admin";
|
public static adminAuthRole = 'admin'
|
||||||
|
|
||||||
constructor(private readonly cookies: Cookies) {
|
constructor(
|
||||||
this.cookieValue = this.cookies.get(CookieAuthentication.cookieName) ?? "";
|
private readonly cookies: Cookies,
|
||||||
this.cookieValueArray = this.cookieValue.split(",");
|
){
|
||||||
|
this.cookieValue = this.cookies.get(CookieAuthentication.cookieName) ?? ''
|
||||||
|
this.cookieValueArray = this.cookieValue.split(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isAuthdAsAdmin(): boolean {
|
public get isAuthdAsAdmin(): boolean {
|
||||||
let isAuthdAsAdmin = false;
|
let isAuthdAsAdmin = false;
|
||||||
|
|
||||||
if (this.cookieValueArray.includes(CookieAuthentication.adminAuthRole)) {
|
if (this.cookieValueArray.includes(CookieAuthentication.adminAuthRole)) {
|
||||||
isAuthdAsAdmin = true;
|
isAuthdAsAdmin = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return isAuthdAsAdmin;
|
return isAuthdAsAdmin
|
||||||
}
|
|
||||||
|
|
||||||
public logout() {
|
|
||||||
if (!this.isAuthdAsAdmin) return;
|
|
||||||
|
|
||||||
this.cookies.delete(CookieAuthentication.cookieName, { path: "/" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAdminAuthentication(isAuthd: boolean) {
|
public setAdminAuthentication(isAuthd: boolean) {
|
||||||
let value = this.cookieValue;
|
let value = this.cookieValue
|
||||||
|
|
||||||
if (isAuthd) {
|
if (isAuthd) {
|
||||||
value = Array.from(
|
value = Array.from(new Set([...this.cookieValueArray, CookieAuthentication.adminAuthRole])).join(',')
|
||||||
new Set([...this.cookieValueArray, CookieAuthentication.adminAuthRole]),
|
|
||||||
).join(",");
|
|
||||||
} else {
|
} else {
|
||||||
value = this.cookieValueArray
|
value = this.cookieValueArray.filter((i) => i !== CookieAuthentication.adminAuthRole).join(',')
|
||||||
.filter((i) => i !== CookieAuthentication.adminAuthRole)
|
|
||||||
.join(",");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cookies.set(CookieAuthentication.cookieName, value, { path: "/" });
|
this.cookies.set(CookieAuthentication.cookieName, value, { path: '/'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { page } from "$app/state";
|
|
||||||
|
|
||||||
const { children } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="admin-layout">
|
|
||||||
{#if page.url.pathname !== "/admin/login"}
|
|
||||||
<nav class="navbar">
|
|
||||||
<menu class="left">
|
|
||||||
<li><a href="/admin/photos">Photos</a></li>
|
|
||||||
<li><a href="/admin/photos/upload">Upload a Photo</a></li>
|
|
||||||
<li><a href="/blog/new">Write a new blog post</a></li>
|
|
||||||
</menu>
|
|
||||||
<menu class="right">
|
|
||||||
<li>
|
|
||||||
<a href="/admin/logout">Log Out</a>
|
|
||||||
</li>
|
|
||||||
</menu>
|
|
||||||
</nav>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.admin-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: min-content auto;
|
|
||||||
min-height: 100dvh;
|
|
||||||
}
|
|
||||||
.navbar {
|
|
||||||
width: 100dvw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 4px;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar menu li {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li a::after {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
<h1>Welcome home, Wilson</h1>
|
<h1>Welcome home, Wilson</h1>
|
||||||
|
|
||||||
<a href="/admin/photos">Upload Photo</a>
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,11 @@
|
||||||
<h1>Admin login</h1>
|
<h1>Admin login</h1>
|
||||||
|
|
||||||
<div class="container">
|
<form method="POST">
|
||||||
<form method="POST" class="form">
|
<div class="field">
|
||||||
<div class="field">
|
<label for="token">API Token</label>
|
||||||
<label for="token">Password</label>
|
<input type="password" id="token" name="token" />
|
||||||
<input type="password" id="token" name="token" />
|
</div>
|
||||||
</div>
|
<div class="field">
|
||||||
<div class="field">
|
<input type="submit" value="Login">
|
||||||
<input type="submit" value="Login" class="thomaswilson-button" />
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
width: 560px;
|
|
||||||
max-width: 90dvw;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field label {
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
|
|
||||||
import { redirect, type ServerLoad } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export const GET: ServerLoad = ({ cookies }) => {
|
|
||||||
new CookieAuthentication(cookies).logout();
|
|
||||||
redirect(307, "/");
|
|
||||||
};
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import { writeFile } from "node:fs/promises";
|
|
||||||
import { Buffer } from "node:buffer";
|
|
||||||
import { join } from "node:path";
|
|
||||||
|
|
||||||
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private";
|
|
||||||
import type { Actions, ServerLoad } from "@sveltejs/kit";
|
|
||||||
import { randomUUID } from "crypto";
|
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ locals }) => {
|
|
||||||
const photos = await locals.prisma.photoPost.findMany({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
fileName: true,
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return { photos };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
default: async ({ request, locals }) => {
|
|
||||||
const formData = await request.formData();
|
|
||||||
const file = formData.get("file") as File;
|
|
||||||
const title = formData.get("title") as string;
|
|
||||||
const description = formData.get("description") as string;
|
|
||||||
|
|
||||||
const filetype = file.type.split("/")[1];
|
|
||||||
const fileName = `${randomUUID()}.${filetype}`;
|
|
||||||
const fileLocation = join(PRIVATE_PHOTO_UPLOAD_DIR, fileName);
|
|
||||||
|
|
||||||
const fileContentBuffer = await file.arrayBuffer();
|
|
||||||
await writeFile(fileLocation, Buffer.from(fileContentBuffer));
|
|
||||||
|
|
||||||
await locals.prisma.photoPost.create({
|
|
||||||
data: {
|
|
||||||
fileName,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
createdAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
},
|
|
||||||
} satisfies Actions;
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import PhotoPostForm from "../../../components/admin/PhotoPostForm.svelte";
|
|
||||||
import type { PageProps } from "./$types.js";
|
|
||||||
import PhotoFeed from "./PhotoFeed.svelte";
|
|
||||||
|
|
||||||
const { data }: PageProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1>Photo Posts</h1>
|
|
||||||
<PhotoFeed photos={data.photos} />
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { PhotoPost } from "../../../../generated/prisma/client.js";
|
|
||||||
|
|
||||||
const { photos }: { photos: PhotoPost[] } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ul class="photo-feed">
|
|
||||||
{#each photos as photo, id}
|
|
||||||
<li class="item">
|
|
||||||
<img width="250" src={`/image/${photo.fileName}`} alt={photo.title} />
|
|
||||||
<p>{photo.title}</p>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.photo-feed {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item img {
|
|
||||||
width: 250px;
|
|
||||||
height: auto;
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script>
|
|
||||||
import PhotoPostForm from "../../../../components/admin/PhotoPostForm.svelte";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="form-container">
|
|
||||||
<h1>Upload a Photo</h1>
|
|
||||||
<PhotoPostForm />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { access, readFile } from "node:fs/promises";
|
|
||||||
import { constants } from "node:fs";
|
|
||||||
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { error, type ServerLoad } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export const GET: ServerLoad = async ({ params, locals }) => {
|
|
||||||
const { filename } = params;
|
|
||||||
|
|
||||||
const proposedFilePath = path.join(PRIVATE_PHOTO_UPLOAD_DIR, filename);
|
|
||||||
|
|
||||||
const fileExists = await access(proposedFilePath, constants.F_OK)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false);
|
|
||||||
|
|
||||||
if (!fileExists) {
|
|
||||||
return error(404, "File not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = await readFile(proposedFilePath);
|
|
||||||
const fileExt = path.extname(filename);
|
|
||||||
const contentType = `image/${fileExt.replace(".", "")}`;
|
|
||||||
|
|
||||||
return new Response(file, { headers: { "Content-Type": contentType } });
|
|
||||||
};
|
|
||||||
|
|
@ -213,50 +213,3 @@ a.no-icon::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* An Alert-like component*/
|
|
||||||
.alert {
|
|
||||||
--colour-scheme-border: var(--gray-800);
|
|
||||||
--font-size: var(--font-size-base);
|
|
||||||
--colour-scheme-text: var(--gray-800);
|
|
||||||
--colour-scheme-bg: var(--gray-100);
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid var(--colour-scheme-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: var(--font-size);
|
|
||||||
letter-spacing: 0px;
|
|
||||||
color: var(--colour-scheme-text);
|
|
||||||
background-color: var(--colour-scheme-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert.error {
|
|
||||||
--colour-scheme-border: var(--red-800);
|
|
||||||
--colour-scheme-text: var(--red-800);
|
|
||||||
--colour-scheme-bg: var(--red-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-form .field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.admin-form .field label {
|
|
||||||
font-size: var(--font-size);
|
|
||||||
letter-spacing: 0px;
|
|
||||||
color: var(--colour-scheme-text);
|
|
||||||
}
|
|
||||||
.admin-form .field input {
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid var(--colour-scheme-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: var(--font-size);
|
|
||||||
letter-spacing: 0px;
|
|
||||||
color: var(--colour-scheme-text);
|
|
||||||
background-color: var(--colour-scheme-bg);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,7 @@ const config = {
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter({ split: false }),
|
adapter: adapter({ split: false }),
|
||||||
alias: {
|
|
||||||
$lib: "/src/lib",
|
|
||||||
$srcPrisma: "/src/prisma",
|
|
||||||
$generatedPrisma: "/generated/prisma/*",
|
|
||||||
},
|
|
||||||
env: {
|
env: {
|
||||||
publicPrefix: "PUBLIC_",
|
publicPrefix: "PUBLIC_",
|
||||||
privatePrefix: "PRIVATE_",
|
privatePrefix: "PRIVATE_",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue