Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add referential actions to the database #91

Merged
merged 2 commits into from
Jan 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_user_group_membership" (
"id" TEXT NOT NULL PRIMARY KEY,
"active" BOOLEAN NOT NULL DEFAULT false,
"userId" TEXT NOT NULL,
"groupId" TEXT NOT NULL,
"roleId" INTEGER NOT NULL DEFAULT 1,
CONSTRAINT "user_group_membership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "user_group_membership_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "group" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "user_group_membership_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_user_group_membership" ("active", "groupId", "id", "roleId", "userId") SELECT "active", "groupId", "id", "roleId", "userId" FROM "user_group_membership";
DROP TABLE "user_group_membership";
ALTER TABLE "new_user_group_membership" RENAME TO "user_group_membership";
CREATE UNIQUE INDEX "user_group_membership_id_key" ON "user_group_membership"("id");
CREATE TABLE "new_items" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"price" TEXT,
"url" TEXT,
"note" TEXT,
"image_url" TEXT,
"userId" TEXT NOT NULL,
"addedById" TEXT NOT NULL,
"pledgedById" TEXT,
"approved" BOOLEAN NOT NULL DEFAULT true,
"purchased" BOOLEAN NOT NULL DEFAULT false,
"groupId" TEXT,
CONSTRAINT "items_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "items_addedById_fkey" FOREIGN KEY ("addedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "items_pledgedById_fkey" FOREIGN KEY ("pledgedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "items_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "group" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_items" ("addedById", "approved", "groupId", "id", "image_url", "name", "note", "pledgedById", "price", "purchased", "url", "userId") SELECT "addedById", "approved", "groupId", "id", "image_url", "name", "note", "pledgedById", "price", "purchased", "url", "userId" FROM "items";
DROP TABLE "items";
ALTER TABLE "new_items" RENAME TO "items";
CREATE UNIQUE INDEX "items_id_key" ON "items"("id");
CREATE INDEX "items_userId_idx" ON "items"("userId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
12 changes: 6 additions & 6 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ model Group {
model UserGroupMembership {
id String @id @unique @default(uuid())
active Boolean @default(false)
user User @relation(fields: [userId], references: [id])
group Group @relation(fields: [groupId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id])
userId String
groupId String
Expand All @@ -83,15 +83,15 @@ model Item {
url String?
note String?
image_url String?
user User @relation(name: "MyItems", fields: [userId], references: [id])
user User @relation(name: "MyItems", fields: [userId], references: [id], onDelete: Cascade)
userId String
addedBy User @relation(name: "AddedItems", fields: [addedById], references: [id])
addedBy User @relation(name: "AddedItems", fields: [addedById], references: [id], onDelete: Cascade)
addedById String
pledgedBy User? @relation(name: "PledgedItems", fields: [pledgedById], references: [id])
pledgedBy User? @relation(name: "PledgedItems", fields: [pledgedById], references: [id], onDelete: SetNull)
pledgedById String?
approved Boolean @default(true)
purchased Boolean @default(false)
group Group? @relation(fields: [groupId], references: [id])
group Group? @relation(fields: [groupId], references: [id], onDelete: Cascade)
groupId String?

@@index([userId])
Expand Down
17 changes: 17 additions & 0 deletions src/lib/server/image-util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sharp from "sharp";
import { unlink } from "fs/promises";

export const createImage = async (username: string, image: File): Promise<string | null> => {
let filename = null;
Expand All @@ -13,3 +14,19 @@ export const createImage = async (username: string, image: File): Promise<string

return filename;
};

export const deleteImage = async (filename: string): Promise<void> => {
try {
await unlink(`uploads/${filename}`);
} catch (e) {
console.warn("Unable to delete file: ", filename);
}
};

export const tryDeleteImage = async (imageUrl: string): Promise<void> => {
try {
new URL(imageUrl);
} catch {
await deleteImage(imageUrl);
}
};
46 changes: 27 additions & 19 deletions src/lib/server/invite-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,26 @@ import generateToken, { hashToken } from "./token";
export const inviteUser = async ({ url, request }: RequestEvent) => {
const token = await generateToken();
const tokenUrl = new URL(`/signup?token=${token}`, url);
const formData = Object.fromEntries(await request.formData());
let schema;

const config = await getConfig();

if (!config.smtp.enable) {
await client.signupToken.create({
data: {
hashedToken: hashToken(token)
}
if (config.smtp.enable) {
schema = z.object({
"invite-email": z.string().email(),
"invite-group": z.string().optional()
});
} else {
schema = z.object({
"invite-email": z.string().optional(),
"invite-group": z.string().optional()
});

return { action: "invite-email", success: true, url: tokenUrl.href };
}

const formData = Object.fromEntries(await request.formData());
const schema = z.object({
"invite-email": z.string().email(),
"invite-group": z.string().min(1)
});
const data = schema.safeParse(formData);

const emailData = schema.safeParse(formData);

if (!emailData.success) {
const errors = emailData.error.errors.map((error) => {
if (!data.success) {
const errors = data.error.errors.map((error) => {
return {
field: error.path[0],
message: error.message
Expand All @@ -39,13 +36,24 @@ export const inviteUser = async ({ url, request }: RequestEvent) => {
return fail(400, { action: "invite-email", error: true, errors });
}

if (!config.smtp.enable) {
await client.signupToken.create({
data: {
hashedToken: hashToken(token),
groupId: data.data["invite-group"]
}
});

return { action: "invite-email", success: true, url: tokenUrl.href };
}

await client.signupToken.create({
data: {
hashedToken: hashToken(token),
groupId: emailData.data["invite-group"]
groupId: data.data["invite-group"]
}
});

await sendSignupLink(emailData.data["invite-email"], tokenUrl.href);
await sendSignupLink(data.data["invite-email"]!, tokenUrl.href);
return { action: "invite-email", success: true, url: null };
};
13 changes: 12 additions & 1 deletion src/routes/account/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fail, redirect } from "@sveltejs/kit";
import { z } from "zod";
import type { Actions, PageServerLoad } from "./$types";
import type { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { createImage } from "$lib/server/image-util";
import { createImage, tryDeleteImage } from "$lib/server/image-util";

export const load: PageServerLoad = async ({ locals }) => {
const session = await locals.validate();
Expand Down Expand Up @@ -73,6 +73,14 @@ export const actions: Actions = {
const filename = await createImage(session.user.username, image);

if (filename) {
const user = await client.user.findUniqueOrThrow({
select: {
picture: true
},
where: {
id: session.user.userId
}
});
await client.user.update({
where: {
id: session.user.userId
Expand All @@ -81,6 +89,9 @@ export const actions: Actions = {
picture: filename
}
});
if (user.picture) {
await tryDeleteImage(user.picture);
}
}
},

Expand Down
2 changes: 1 addition & 1 deletion src/routes/admin/users/[username]/+page@.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
invalidateAll();

toastStore.trigger({
message: `${userId} was deleted`,
message: `${username} was deleted`,
autohide: true,
timeout: 5000
});
Expand Down
24 changes: 22 additions & 2 deletions src/routes/api/items/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { client } from "$lib/server/prisma";
import { error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { _authCheck } from "../groups/[groupId]/auth";
import { tryDeleteImage } from "$lib/server/image-util";

export const DELETE: RequestHandler = async ({ locals, request }) => {
const groupId = new URL(request.url).searchParams.get("groupId");
Expand All @@ -23,13 +24,32 @@ export const DELETE: RequestHandler = async ({ locals, request }) => {
}

try {
const items = await client.item.deleteMany({
const items = await client.item.findMany({
select: {
id: true,
image_url: true
},
where: {
groupId: groupId ? groupId : undefined,
pledgedById: claimed && Boolean(claimed) ? { not: null } : undefined
}
});
return new Response(JSON.stringify(items), { status: 200 });

for (const item of items) {
if (item.image_url) {
await tryDeleteImage(item.image_url);
}
}

const deletedItems = await client.item.deleteMany({
where: {
id: {
in: items.map((item) => item.id)
}
}
});

return new Response(JSON.stringify(deletedItems), { status: 200 });
} catch (e) {
error(500, "Unable to delete items");
}
Expand Down
27 changes: 21 additions & 6 deletions src/routes/api/items/[itemId]/+server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getConfig } from "$lib/server/config";
import { getActiveMembership } from "$lib/server/group-membership";
import { tryDeleteImage } from "$lib/server/image-util";
import { client } from "$lib/server/prisma";
import { error, type RequestHandler } from "@sveltejs/kit";
import assert from "assert";
Expand Down Expand Up @@ -28,7 +29,8 @@ const validateItem = async (itemId: string | undefined, session: Session | null)
select: {
username: true
}
}
},
image_url: true
}
});

Expand Down Expand Up @@ -68,10 +70,15 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
select: {
username: true
}
}
},
image_url: true
}
});

if (item.image_url) {
await tryDeleteImage(item.image_url);
}

return new Response(JSON.stringify(item), { status: 200 });
} catch (e) {
error(404, "item id not found");
Expand All @@ -81,7 +88,7 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
export const PATCH: RequestHandler = async ({ params, locals, request }) => {
const session = await locals.validate();

await validateItem(params?.itemId, session);
const item = await validateItem(params?.itemId, session);

const body = (await request.json()) as Record<string, unknown>;
const data: {
Expand All @@ -97,12 +104,16 @@ export const PATCH: RequestHandler = async ({ params, locals, request }) => {
approved?: boolean;
purchased?: boolean;
} = {};
let deleteOldImage = false;

if (body.name && typeof body.name === "string") data.name = body.name;
if (body.price && typeof body.price === "string") data.price = body.price;
if (body.url && typeof body.url === "string") data.url = body.url;
if (body.note && typeof body.note === "string") data.note = body.note;
if (body.image_url && typeof body.image_url === "string") data.image_url = body.image_url;
if (body.image_url && typeof body.image_url === "string") {
data.image_url = body.image_url;
deleteOldImage = true;
}
if (body.pledgedById && typeof body.pledgedById === "string") {
if (body.pledgedById === "0") {
data.pledgedBy = {
Expand All @@ -120,15 +131,19 @@ export const PATCH: RequestHandler = async ({ params, locals, request }) => {
if (Object.keys(body).includes("purchased") && typeof body.purchased === "boolean") data.purchased = body.purchased;

try {
const item = await client.item.update({
const updatedItem = await client.item.update({
where: {
// @ts-expect-error params.itemId is checked in a previous function
id: parseInt(params.itemId)
},
data
});

return new Response(JSON.stringify(item), { status: 200 });
if (deleteOldImage && item.image_url) {
await tryDeleteImage(item.image_url);
}

return new Response(JSON.stringify(updatedItem), { status: 200 });
} catch (e) {
error(404, "item id not found");
}
Expand Down
9 changes: 4 additions & 5 deletions src/routes/api/users/[userId]/+server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Role } from "$lib/schema";
import { tryDeleteImage } from "$lib/server/image-util";
import { client } from "$lib/server/prisma";
import { type RequestHandler, error } from "@sveltejs/kit";

Expand Down Expand Up @@ -30,16 +31,14 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
}

try {
await client.userGroupMembership.deleteMany({
where: {
userId: user.id
}
});
const deletedUser = await client.user.delete({
where: {
id: user.id
}
});
if (deletedUser && deletedUser.picture) {
await tryDeleteImage(deletedUser.picture);
}

return new Response(JSON.stringify(deletedUser), { status: 200 });
} catch (e) {
Expand Down
12 changes: 11 additions & 1 deletion src/routes/wishlists/[username]/edit/[itemId]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Actions, PageServerLoad } from "./$types";
import { client } from "$lib/server/prisma";
import { getConfig } from "$lib/server/config";
import { getActiveMembership } from "$lib/server/group-membership";
import { createImage } from "$lib/server/image-util";
import { createImage, tryDeleteImage } from "$lib/server/image-util";

export const load: PageServerLoad = async ({ locals, params }) => {
const session = await locals.validate();
Expand Down Expand Up @@ -74,6 +74,12 @@ export const actions: Actions = {

const filename = await createImage(session.user.username, image);

const item = await client.item.findUniqueOrThrow({
where: {
id: parseInt(params.itemId)
}
});

await client.item.update({
where: {
id: parseInt(params.itemId)
Expand All @@ -87,6 +93,10 @@ export const actions: Actions = {
}
});

if (filename && item.image_url && item.image_url !== filename) {
await tryDeleteImage(item.image_url);
}

const ref = new URL(request.url).searchParams.get("ref");
redirect(302, ref ?? `/wishlists/${params.username}`);
}
Expand Down
Loading