Skip to content

Commit

Permalink
authenticate sse endpoint and add checks to only recieve updates that…
Browse files Browse the repository at this point in the history
… are relevant (#104)
  • Loading branch information
cmintey authored Jan 28, 2024
1 parent b5eb6cb commit 648908f
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 59 deletions.
12 changes: 2 additions & 10 deletions src/lib/components/wishlists/ItemCard/ItemCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
</script>

<script lang="ts">
import { invalidateAll } from "$app/navigation";
import {
getDrawerStore,
getModalStore,
Expand All @@ -23,6 +22,7 @@
import { ItemAPI } from "$lib/api/items";
import ApprovalButtons from "./ApprovalButtons.svelte";
import ClaimButtons from "./ClaimButtons.svelte";
import { invalidateAll } from "$app/navigation";
export let item: FullItem;
export let user: PartialUser & { userId: string };
Expand Down Expand Up @@ -89,8 +89,6 @@
const resp = await (approve ? itemAPI.approve() : itemAPI.deny());
if (resp.ok) {
invalidateAll();
toastStore.trigger({
message: `${item.name} was ${approve ? "approved" : "denied"}`,
autohide: true,
Expand Down Expand Up @@ -121,8 +119,6 @@
const resp = await (unclaim ? itemAPI.unclaim() : itemAPI.claim(user.userId));
if (resp.ok) {
invalidateAll();
toastStore.trigger({
message: `${unclaim ? "Unclaimed" : "Claimed"} item`,
autohide: true,
Expand All @@ -134,11 +130,7 @@
};
const handlePurchased = async (purchased: boolean) => {
const resp = await (purchased ? itemAPI.purchase() : itemAPI.unpurchase());
if (resp.ok) {
invalidateAll();
}
await (purchased ? itemAPI.purchase() : itemAPI.unpurchase());
};
</script>

Expand Down
10 changes: 6 additions & 4 deletions src/lib/server/events/sse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// https://github.com/sanrafa/sveltekit-sse-example/tree/main
import type EventEmitter from "node:events";

export function createSSE(retry = 0) {
export function createSSE<T>(retry = 0) {
const { readable, writable } = new TransformStream({
start(ctr) {
if (retry > 0) ctr.enqueue(`retry: ${retry}\n\n`);
Expand All @@ -22,9 +22,11 @@ export function createSSE(retry = 0) {

return {
readable,
async subscribeToEvent(emitter: EventEmitter, event: string) {
function listener(data: any) {
writer.write({ event, data });
async subscribeToEvent(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean) {
function listener(data: T) {
if (!predicate || (predicate && predicate(data))) {
writer.write({ event, data });
}
}
emitter.on(event, listener);
await writer.closed.catch((e) => {
Expand Down
12 changes: 8 additions & 4 deletions src/routes/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { PageServerLoad } from "./$types";

import { client } from "$lib/server/prisma";
import { getActiveMembership } from "$lib/server/group-membership";
import { getConfig } from "$lib/server/config";

export const load = (async ({ locals }) => {
const session = await locals.validate();
Expand All @@ -12,6 +13,7 @@ export const load = (async ({ locals }) => {
const user = session.user;

const activeMembership = await getActiveMembership(user);
const config = await getConfig(activeMembership.groupId);

const userQuery = client.user.findUniqueOrThrow({
select: {
Expand All @@ -23,9 +25,10 @@ export const load = (async ({ locals }) => {
id: true
},
where: {
addedBy: {
username: user.username
},
addedBy:
config.suggestions.enable && config.suggestions.method === "surprise"
? { username: user.username }
: undefined,
groupId: activeMembership.groupId
}
},
Expand Down Expand Up @@ -68,7 +71,8 @@ export const load = (async ({ locals }) => {
id: true
},
where: {
groupId: activeMembership.groupId
groupId: activeMembership.groupId,
approved: true
}
},
_count: {
Expand Down
18 changes: 0 additions & 18 deletions src/routes/api/events/+server.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/routes/api/items/[itemId]/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
},
select: {
id: true,
groupId: true,
userId: true,
addedBy: {
select: {
username: true
Expand Down
3 changes: 1 addition & 2 deletions src/routes/wishlists/[username]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,7 @@ export const load: PageServerLoad = async ({ locals, params, url }) => {
isMe: params.username === session.user.username,
...listOwner
},
items: wishlistItems.filter((item) => item.approved),
approvals: wishlistItems.filter((item) => !item.approved),
items: wishlistItems,
suggestionsEnabled: config.suggestions.enable,
showClaimedName: config.claims.showName,
groupId: activeMembership.groupId
Expand Down
60 changes: 39 additions & 21 deletions src/routes/wishlists/[username]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
export let data: PageData;
type Item = PageData["items"][0];
$: allItems = data.items;
$: approvals = allItems.filter((item) => !item.approved);
$: items = allItems.filter((item) => item.approved);
const [send, receive] = crossfade({
duration: (d) => Math.sqrt(d * 200),
Expand All @@ -37,31 +40,41 @@
let eventSource: EventSource;
onMount(async () => {
const userHash = await hash(data.listOwner.id + data.groupId);
$viewedItems[userHash] = await hashItems(data.items);
await updateHash();
subscribeToEvents();
});
onDestroy(() => eventSource?.close());
const updateHash = async () => {
const userHash = await hash(data.listOwner.id + data.groupId);
$viewedItems[userHash] = await hashItems(allItems);
};
const subscribeToEvents = () => {
eventSource = new EventSource("/api/events");
eventSource = new EventSource(`${$page.url.pathname}/events`);
eventSource.addEventListener(SSEvents.item.update, (e) => {
const message = JSON.parse(e.data) as Item;
updateItems(message);
updateHash();
});
eventSource.addEventListener(SSEvents.item.delete, (e) => {
const message = JSON.parse(e.data) as Item;
removeItem(message);
updateHash();
});
eventSource.addEventListener(SSEvents.item.create, (e) => {
const message = JSON.parse(e.data) as Item;
addItem(message);
updateHash();
});
};
const updateItems = (updatedItem: Item) => {
data.items = data.items.map((item) => {
// for when an item gets approved
if (!allItems.find((item) => item.id === updatedItem.id)) {
addItem(updatedItem);
}
allItems = allItems.map((item) => {
if (item.id === updatedItem.id) {
return { ...item, ...updatedItem };
}
Expand All @@ -70,18 +83,31 @@
};
const removeItem = (removedItem: Item) => {
data.items = data.items.filter((item) => item.id !== removedItem.id);
allItems = allItems.filter((item) => item.id !== removedItem.id);
};
const addItem = (addedItem: Item) => {
data.items = [...data.items, addedItem];
if (!(addedItem.approved || data.listOwner.isMe)) {
return;
}
allItems = [...allItems, addedItem];
};
</script>

{#if data.approvals.length > 0}
<!-- chips -->
{#if allItems.length > 0}
<div class="flex flex-row flex-wrap space-x-4">
{#if !data.listOwner.isMe}
<ClaimFilterChip />
{/if}
<SortBy />
</div>
{/if}

{#if approvals.length > 0}
<h2 class="h2 pb-2">Approvals</h2>
<div class="flex flex-col space-y-4 pb-2">
{#each data.approvals as item (item.id)}
{#each approvals as item (item.id)}
<div in:receive={{ key: item.id }} out:send|local={{ key: item.id }} animate:flip={{ duration: 200 }}>
<ItemCard {item} showClaimedName={data.showClaimedName} user={data.user} />
</div>
Expand All @@ -90,37 +116,29 @@
<hr class="pb-2" />
{/if}

{#if data.items.length === 0}
{#if items.length === 0}
<div class="flex flex-col items-center justify-center space-y-4 pt-4">
<img class="w-3/4 md:w-1/3" alt="Two people looking in an empty box" src={empty} />
<p class="text-2xl">No wishes yet</p>
</div>
{:else}
<!-- chips -->
<div class="flex flex-row flex-wrap space-x-4">
{#if !data.listOwner.isMe}
<ClaimFilterChip />
{/if}
<SortBy />
</div>

<!-- items -->
<div class="flex flex-col space-y-4">
{#if data.listOwner.isMe}
{#each data.items as item (item.id)}
{#each items as item (item.id)}
<div in:receive={{ key: item.id }} out:send|local={{ key: item.id }} animate:flip={{ duration: 200 }}>
<ItemCard {item} showClaimedName={data.showClaimedName} user={data.user} />
</div>
{/each}
{:else}
<!-- unclaimed-->
{#each data.items.filter((item) => !item.pledgedById) as item (item.id)}
{#each items.filter((item) => !item.pledgedById) as item (item.id)}
<div in:receive={{ key: item.id }} out:send|local={{ key: item.id }} animate:flip={{ duration: 200 }}>
<ItemCard {item} showClaimedName={data.showClaimedName} user={data.user} />
</div>
{/each}
<!-- claimed -->
{#each data.items.filter((item) => item.pledgedById) as item (item.id)}
{#each items.filter((item) => item.pledgedById) as item (item.id)}
<div in:receive={{ key: item.id }} out:send|local={{ key: item.id }} animate:flip={{ duration: 200 }}>
<ItemCard {item} showClaimedName={data.showClaimedName} user={data.user} />
</div>
Expand Down
63 changes: 63 additions & 0 deletions src/routes/wishlists/[username]/events/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { SSEvents } from "$lib/schema";
import { itemEmitter } from "$lib/server/events/emitters";
import { createSSE } from "$lib/server/events/sse";
import type { Item } from "@prisma/client";
import type { RequestHandler } from "./$types";
import { error } from "@sveltejs/kit";
import { client } from "$lib/server/prisma";
import { getConfig } from "$lib/server/config";

export const GET = (async ({ locals, params }) => {
const session = await locals.validate();
if (!session) {
error(401, "Unauthorized");
}

const [wishlistUser, activeGroup] = await Promise.all([
client.user.findUniqueOrThrow({
select: {
id: true
},
where: {
username: params.username
}
}),
client.userGroupMembership.findFirstOrThrow({
select: {
groupId: true
},
where: {
userId: session.user.userId,
active: true
}
})
]);

const config = await getConfig(activeGroup.groupId);
if (
config.suggestions.enable &&
config.suggestions.method === "surprise" &&
params.username === session.user.username
) {
return new Response();
}

const predicate = (item: Item) => {
const b = activeGroup.groupId === item.groupId && wishlistUser.id === item.userId;
return b;
};

const { readable, subscribeToEvent } = createSSE<Item>();

subscribeToEvent(itemEmitter, SSEvents.item.update, predicate);
subscribeToEvent(itemEmitter, SSEvents.item.create, predicate);
subscribeToEvent(itemEmitter, SSEvents.item.delete, predicate);

return new Response(readable, {
headers: {
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
"X-Accel-Buffering": "no"
}
});
}) satisfies RequestHandler;

0 comments on commit 648908f

Please sign in to comment.