mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-05 13:15:18 +02:00
commit
0ba0ef848b
11 changed files with 1496 additions and 21 deletions
|
@ -1 +1,9 @@
|
||||||
DATABASE_URL=
|
DATABASE_URL=
|
||||||
|
|
||||||
|
MINIO_SERVER_URL=http://localhost:9000
|
||||||
|
MINIO_CLIENT_URL=http://localhost:9000
|
||||||
|
MINIO_ENDPOINT=localhost
|
||||||
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
|
MINIO_SECRET_KEY=minioadmin
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
BODY_SIZE_LIMIT=Infinity
|
|
@ -7,10 +7,18 @@ services:
|
||||||
- DATABASE_URL=postgres://adventurelog:PO24VjITwGgk@db:5432/adventurelog
|
- DATABASE_URL=postgres://adventurelog:PO24VjITwGgk@db:5432/adventurelog
|
||||||
# ORIGIN is only necessary when not using a reverse proxy or hosting that includes https
|
# ORIGIN is only necessary when not using a reverse proxy or hosting that includes https
|
||||||
- ORIGIN=http://localhost:3000
|
- ORIGIN=http://localhost:3000
|
||||||
|
# SKIP_DB_WAIT: Only necessary for externally hosted databases such as NeonDB which have their own health checks!
|
||||||
- SKIP_DB_WAIT=false
|
- SKIP_DB_WAIT=false
|
||||||
# Only necessary for externaly hosted databases such as NeonDB
|
- AWS_ACCESS_KEY_ID=minioadmin
|
||||||
|
- AWS_SECRET_ACCESS_KEY=minioadmin
|
||||||
|
- AWS_S3_ENDPOINT=http://minio:9000
|
||||||
|
# MINIO_CLIENT_OVERRIDE: Only necessary if using minio here with this docker compose file. This is becaues the client needs a different endpoint than the server because its not in the docker network.
|
||||||
|
- MINIO_CLIENT_OVERRIDE=http://localhost:9000
|
||||||
|
- BODY_SIZE_LIMIT=Infinity # change this to a smaller value if you want to limit the size of uploaded files!
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
- minio
|
||||||
db:
|
db:
|
||||||
image: postgres
|
image: postgres
|
||||||
environment:
|
environment:
|
||||||
|
@ -19,3 +27,17 @@ services:
|
||||||
POSTGRES_DB: adventurelog
|
POSTGRES_DB: adventurelog
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
minio:
|
||||||
|
image: quay.io/minio/minio
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
- MINIO_ROOT_USER=minioadmin
|
||||||
|
- MINIO_ROOT_PASSWORD=minioadmin
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
minio_data:
|
||||||
|
|
17
minio/docker-compose.yml
Normal file
17
minio/docker-compose.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
version: "3.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
minio:
|
||||||
|
image: quay.io/minio/minio
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
- MINIO_ROOT_USER=minioadmin
|
||||||
|
- MINIO_ROOT_PASSWORD=minioadmin
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
ports:
|
||||||
|
- 9000:9000
|
||||||
|
- 9001:9001
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
minio_data:
|
|
@ -38,6 +38,7 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.592.0",
|
||||||
"@lucia-auth/adapter-drizzle": "^1.0.7",
|
"@lucia-auth/adapter-drizzle": "^1.0.7",
|
||||||
"@vercel/analytics": "^1.2.2",
|
"@vercel/analytics": "^1.2.2",
|
||||||
"@vercel/speed-insights": "^1.0.10",
|
"@vercel/speed-insights": "^1.0.10",
|
||||||
|
|
1211
pnpm-lock.yaml
generated
1211
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -22,7 +22,7 @@
|
||||||
<div class="dropdown dropdown-bottom dropdown-end" tabindex="0" role="button">
|
<div class="dropdown dropdown-bottom dropdown-end" tabindex="0" role="button">
|
||||||
<div class="avatar placeholder">
|
<div class="avatar placeholder">
|
||||||
<div class="bg-neutral text-neutral-content rounded-full w-10 ml-4">
|
<div class="bg-neutral text-neutral-content rounded-full w-10 ml-4">
|
||||||
<span class="text-2xl -mt-0.5">{icon}</span>
|
<img src={user.icon} alt="" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- svelte-ignore a11y-missing-attribute -->
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
|
|
121
src/lib/server/s3.ts
Normal file
121
src/lib/server/s3.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import {
|
||||||
|
CreateBucketCommand,
|
||||||
|
HeadBucketCommand,
|
||||||
|
PutBucketPolicyCommand,
|
||||||
|
PutObjectCommand,
|
||||||
|
S3Client,
|
||||||
|
type S3ClientConfig,
|
||||||
|
} from "@aws-sdk/client-s3";
|
||||||
|
import { env } from "$env/dynamic/private";
|
||||||
|
console.log(env.AWS_ACCESS_KEY_ID as string);
|
||||||
|
|
||||||
|
const s3Config: S3ClientConfig = {
|
||||||
|
region: (env.AWS_REGION as string) || "us-east-1",
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: env.AWS_ACCESS_KEY_ID as string,
|
||||||
|
secretAccessKey: env.AWS_SECRET_ACCESS_KEY as string,
|
||||||
|
},
|
||||||
|
endpoint: env.AWS_S3_ENDPOINT, // Add the endpoint
|
||||||
|
forcePathStyle: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const s3Client = new S3Client(s3Config);
|
||||||
|
|
||||||
|
export const ensureBucketExists = async (bucketName: string): Promise<void> => {
|
||||||
|
const headBucketCommand = new HeadBucketCommand({ Bucket: bucketName });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await s3Client.send(headBucketCommand);
|
||||||
|
console.log(`Bucket ${bucketName} already exists.`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
if (error.$metadata.httpStatusCode === 404) {
|
||||||
|
console.log(`Bucket ${bucketName} does not exist. Creating...`);
|
||||||
|
const createBucketCommand = new CreateBucketCommand({
|
||||||
|
Bucket: bucketName,
|
||||||
|
});
|
||||||
|
await s3Client.send(createBucketCommand);
|
||||||
|
|
||||||
|
// Set a bucket policy to allow public read access
|
||||||
|
const bucketPolicy = {
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: [
|
||||||
|
{
|
||||||
|
Effect: "Allow",
|
||||||
|
Principal: "*", // This allows anyone (public)
|
||||||
|
Action: ["s3:GetBucketLocation", "s3:ListBucket"],
|
||||||
|
Resource: `arn:aws:s3:::${bucketName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Effect: "Allow",
|
||||||
|
Principal: "*", // This allows anyone (public)
|
||||||
|
Action: "s3:GetObject",
|
||||||
|
Resource: `arn:aws:s3:::${bucketName}/*`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const putBucketPolicyCommand = new PutBucketPolicyCommand({
|
||||||
|
Bucket: bucketName,
|
||||||
|
Policy: JSON.stringify(bucketPolicy),
|
||||||
|
});
|
||||||
|
await s3Client.send(putBucketPolicyCommand);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Bucket ${bucketName} created and public read access policy set.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw error; // Rethrow other errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadObject = async (
|
||||||
|
bucketName: string,
|
||||||
|
fileName: string,
|
||||||
|
fileBuffer: Buffer
|
||||||
|
): Promise<string> => {
|
||||||
|
const putObjectCommand = new PutObjectCommand({
|
||||||
|
Bucket: bucketName,
|
||||||
|
Key: fileName,
|
||||||
|
Body: fileBuffer,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await s3Client.send(putObjectCommand);
|
||||||
|
|
||||||
|
// Determine the provider from the endpoint
|
||||||
|
let endpoint = env.AWS_S3_ENDPOINT as string;
|
||||||
|
if (env.MINIO_CLIENT_OVERRIDE) {
|
||||||
|
endpoint = env.MINIO_CLIENT_OVERRIDE;
|
||||||
|
}
|
||||||
|
let objectUrl: string;
|
||||||
|
|
||||||
|
if (endpoint.includes("amazonaws.com")) {
|
||||||
|
// Amazon S3
|
||||||
|
objectUrl = `https://${bucketName}.s3.${env.AWS_REGION}.amazonaws.com/${fileName}`;
|
||||||
|
} else if (endpoint.includes("storage.googleapis.com")) {
|
||||||
|
// Google Cloud Storage
|
||||||
|
objectUrl = `https://storage.googleapis.com/${bucketName}/${fileName}`;
|
||||||
|
} else if (endpoint.includes("digitaloceanspaces.com")) {
|
||||||
|
// DigitalOcean Spaces
|
||||||
|
objectUrl = `https://${bucketName}.${endpoint}/${fileName}`;
|
||||||
|
} else if (endpoint.includes("supabase.co")) {
|
||||||
|
// Supabase Storage
|
||||||
|
endpoint = endpoint.replace("s3", "object/public"); // Remove the version
|
||||||
|
console.log(endpoint);
|
||||||
|
objectUrl = `${endpoint}/${bucketName}/${fileName}`;
|
||||||
|
} else {
|
||||||
|
// Default fallback
|
||||||
|
objectUrl = `${endpoint}/${bucketName}/${fileName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error uploading file ${fileName} to bucket ${bucketName}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
63
src/routes/api/upload/+server.ts
Normal file
63
src/routes/api/upload/+server.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// src/routes/api/upload.js
|
||||||
|
|
||||||
|
import { ensureBucketExists, s3Client, uploadObject } from "$lib/server/s3";
|
||||||
|
import { HeadBucketCommand } from "@aws-sdk/client-s3";
|
||||||
|
import type { RequestEvent } from "@sveltejs/kit";
|
||||||
|
import { generateId } from "lucia";
|
||||||
|
|
||||||
|
export async function POST(event: RequestEvent): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const contentType = event.request.headers.get("content-type") ?? "";
|
||||||
|
const fileExtension = contentType.split("/").pop();
|
||||||
|
const fileName = `${generateId(25)}.${fileExtension}`;
|
||||||
|
|
||||||
|
if (!fileExtension || !fileName) {
|
||||||
|
return new Response(JSON.stringify({ error: "Invalid file type" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the file is an image
|
||||||
|
if (!contentType.startsWith("image")) {
|
||||||
|
return new Response(JSON.stringify({ error: "Invalid file type" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileBuffer = await event.request.arrayBuffer();
|
||||||
|
const metaData = {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
};
|
||||||
|
|
||||||
|
await ensureBucketExists("profile-pics");
|
||||||
|
|
||||||
|
const objectUrl = await uploadObject(
|
||||||
|
"profile-pics",
|
||||||
|
fileName,
|
||||||
|
Buffer.from(fileBuffer)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`File uploaded to ${objectUrl}`);
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ objectUrl }), {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return new Response(JSON.stringify({ error: "Error occured" }), {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,9 @@
|
||||||
import { redirect, type Actions } from "@sveltejs/kit";
|
import {
|
||||||
|
error,
|
||||||
|
redirect,
|
||||||
|
type Actions,
|
||||||
|
type RequestEvent,
|
||||||
|
} from "@sveltejs/kit";
|
||||||
import type { PageServerLoad } from "./$types";
|
import type { PageServerLoad } from "./$types";
|
||||||
import { db } from "$lib/db/db.server";
|
import { db } from "$lib/db/db.server";
|
||||||
import { userTable } from "$lib/db/schema";
|
import { userTable } from "$lib/db/schema";
|
||||||
|
@ -15,15 +20,18 @@ export const load: PageServerLoad = async (event) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async (event: { request: { formData: () => any } }) => {
|
default: async (event: RequestEvent) => {
|
||||||
const formData = await event.request.formData();
|
const formData = (await event.request.formData()) as FormData;
|
||||||
let userId = formData.get("user_id");
|
let userId = formData.get("user_id") as string;
|
||||||
let username = formData.get("username");
|
let username = formData.get("username") as string;
|
||||||
let firstName = formData.get("first_name");
|
let firstName = formData.get("first_name") as string;
|
||||||
let lastName = formData.get("last_name");
|
let lastName = formData.get("last_name") as string;
|
||||||
let icon = formData.get("icon");
|
let icon = event.locals.user?.icon;
|
||||||
|
let profilePicture = formData.get("profilePicture") as File | null;
|
||||||
|
|
||||||
let password = formData.get("password");
|
console.log("PROFILE PICTURE" + profilePicture);
|
||||||
|
|
||||||
|
let password = formData.get("password") as string;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return {
|
return {
|
||||||
|
@ -61,7 +69,6 @@ export const actions: Actions = {
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
let hashedPassword = await new Argon2id().hash(password);
|
let hashedPassword = await new Argon2id().hash(password);
|
||||||
console.log(hashedPassword);
|
|
||||||
await db
|
await db
|
||||||
.update(userTable)
|
.update(userTable)
|
||||||
.set({
|
.set({
|
||||||
|
@ -70,6 +77,23 @@ export const actions: Actions = {
|
||||||
.where(eq(userTable.id, userId));
|
.where(eq(userTable.id, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profilePicture?.size && profilePicture.size > 0) {
|
||||||
|
const response = await event.fetch("/api/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: profilePicture,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("DATA" + data.objectUrl);
|
||||||
|
icon = data.objectUrl;
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
throw error(400, {
|
||||||
|
message: "Error uploading profile picture",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(userTable)
|
.update(userTable)
|
||||||
.set({
|
.set({
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
let username = data.user?.username;
|
let username = data.user?.username;
|
||||||
let first_name = data.user?.first_name;
|
let first_name = data.user?.first_name;
|
||||||
let last_name = data.user?.last_name;
|
let last_name = data.user?.last_name;
|
||||||
|
@ -9,6 +10,8 @@
|
||||||
let icon = data.user?.icon;
|
let icon = data.user?.icon;
|
||||||
let signup_date = data.user?.signup_date;
|
let signup_date = data.user?.signup_date;
|
||||||
let role = data.user?.role;
|
let role = data.user?.role;
|
||||||
|
console.log(username);
|
||||||
|
let file: File | null = null;
|
||||||
|
|
||||||
// the submit function shoud just reload the page
|
// the submit function shoud just reload the page
|
||||||
</script>
|
</script>
|
||||||
|
@ -17,7 +20,12 @@
|
||||||
|
|
||||||
<h1 class="text-center font-extrabold text-xl">User Account Settings</h1>
|
<h1 class="text-center font-extrabold text-xl">User Account Settings</h1>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<form method="post" use:enhance class="w-full max-w-xs">
|
<form
|
||||||
|
method="post"
|
||||||
|
use:enhance
|
||||||
|
class="w-full max-w-xs"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input
|
<input
|
||||||
bind:value={username}
|
bind:value={username}
|
||||||
|
@ -41,12 +49,12 @@
|
||||||
id="last_name"
|
id="last_name"
|
||||||
class="block mb-2 input input-bordered w-full max-w-xs"
|
class="block mb-2 input input-bordered w-full max-w-xs"
|
||||||
/><br />
|
/><br />
|
||||||
<label for="icon">Profile Icon (emoji)</label>
|
<label for="profilePicture">Profile Picture</label>
|
||||||
<input
|
<input
|
||||||
type="emoji"
|
type="file"
|
||||||
bind:value={icon}
|
bind:value={file}
|
||||||
name="icon"
|
name="profilePicture"
|
||||||
id="icon"
|
id="profilePicture"
|
||||||
class="block mb-2 input input-bordered w-full max-w-xs"
|
class="block mb-2 input input-bordered w-full max-w-xs"
|
||||||
/><br />
|
/><br />
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
|
|
|
@ -14,7 +14,7 @@ const config = {
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
csrf: { checkOrigin: true, }
|
csrf: { checkOrigin: true },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue