Skip to content

Commit

Permalink
Public registry mode (#116)
Browse files Browse the repository at this point in the history
* initial backend work

* add functionality

* change uuid to cuid

* prevent adding users in registry mode

* Prevent switching list mode when more than one user in group

* redirect to main list in registry mode

* update id generation

* don't allow access if list was removed from registry mode

* Prevent getting or creating link when not in registry mode

* show claimed by public user if registry mode is de-activated

* Add new lists events path to Caddyfile
  • Loading branch information
cmintey authored Jul 23, 2024
1 parent 34c384e commit 5a433c6
Show file tree
Hide file tree
Showing 35 changed files with 822 additions and 75 deletions.
1 change: 1 addition & 0 deletions Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

@not_sse {
not path /wishlists/*/events
not path /lists/*/events
}

# Handles User Images
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"dependencies": {
"@lucia-auth/adapter-prisma": "^4.0.1",
"@metascraper/helpers": "^5.45.10",
"@paralleldrive/cuid2": "^2.2.2",
"@prisma/client": "^5.17.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions prisma/migrations/20240618010500_add_public_lists/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "public_list" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"groupId" TEXT NOT NULL,
CONSTRAINT "public_list_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "public_list_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "group" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX "public_list_id_key" ON "public_list"("id");

-- CreateIndex
CREATE INDEX "public_list_userId_groupId_idx" ON "public_list"("userId", "groupId");
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- CreateTable
CREATE TABLE "system_user" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"name" TEXT DEFAULT 'Anonymous'
);

-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_items" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"price" TEXT,
"url" TEXT,
"note" TEXT,
"imageUrl" TEXT,
"userId" TEXT NOT NULL,
"addedById" TEXT NOT NULL,
"pledgedById" TEXT,
"publicPledgedById" 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_publicPledgedById_fkey" FOREIGN KEY ("publicPledgedById") REFERENCES "system_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", "imageUrl", "name", "note", "pledgedById", "price", "purchased", "url", "userId") SELECT "addedById", "approved", "groupId", "id", "imageUrl", "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");
CREATE INDEX "items_pledgedById_idx" ON "items"("pledgedById");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

-- CreateIndex
CREATE UNIQUE INDEX "system_user_id_key" ON "system_user"("id");
56 changes: 40 additions & 16 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,20 @@ model User {
roleId Int @default(1)
UserGroupMembership UserGroupMembership[]
hashedPassword String
publicLists PublicList[]
@@map("user")
}

model SystemUser {
id String @id @unique @default(cuid())
username String
name String? @default("Anonymous")
Item Item[] @relation("PublicPledgedItems")
@@map("system_user")
}

model Session {
id String @id @unique
userId String
Expand All @@ -50,6 +60,7 @@ model Group {
name String @unique
UserGroupMembership UserGroupMembership[]
Item Item[]
publicLists PublicList[]
@@map("group")
}
Expand All @@ -71,22 +82,24 @@ model UserGroupMembership {
}

model Item {
id Int @id @unique @default(autoincrement())
name String
price String?
url String?
note String?
imageUrl String?
user User @relation(name: "MyItems", fields: [userId], references: [id], onDelete: Cascade)
userId String
addedBy User @relation(name: "AddedItems", fields: [addedById], references: [id], onDelete: Cascade)
addedById String
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], onDelete: Cascade)
groupId String?
id Int @id @unique @default(autoincrement())
name String
price String?
url String?
note String?
imageUrl String?
user User @relation(name: "MyItems", fields: [userId], references: [id], onDelete: Cascade)
userId String
addedBy User @relation(name: "AddedItems", fields: [addedById], references: [id], onDelete: Cascade)
addedById String
pledgedBy User? @relation(name: "PledgedItems", fields: [pledgedById], references: [id], onDelete: SetNull)
pledgedById String?
publicPledgedBy SystemUser? @relation(name: "PublicPledgedItems", fields: [publicPledgedById], references: [id], onDelete: SetNull)
publicPledgedById String?
approved Boolean @default(true)
purchased Boolean @default(false)
group Group? @relation(fields: [groupId], references: [id], onDelete: Cascade)
groupId String?
@@index([userId])
@@index([pledgedById])
Expand Down Expand Up @@ -123,3 +136,14 @@ model SystemConfig {
@@id([key, groupId])
@@map("system_config")
}

model PublicList {
id String @id @unique @default(cuid())
userId String
groupId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
@@index([userId, groupId])
@@map("public_list")
}
3 changes: 3 additions & 0 deletions src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type ProductData = {

type SuggestionMethod = "surprise" | "auto-approval" | "approval";

type ListMode = "standard" | "registry";

type SMTPConfig =
| {
enable: false;
Expand Down Expand Up @@ -57,6 +59,7 @@ type Config = {
claims: {
showName: boolean;
};
listMode: ListMode;
};

type Option = {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/api/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class ItemAPI {
return await this._makeRequest("PATCH", { pledgedById: "0" });
};

publicClaim = async (systemUserId: string) => {
return await this._makeRequest("PATCH", { publicPledgedById: systemUserId });
};

purchase = async () => {
return await this._makeRequest("PATCH", { purchased: true });
};
Expand Down
35 changes: 35 additions & 0 deletions src/lib/api/lists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export class PublicListAPI {
groupId: string;
constructor(groupId: string) {
this.groupId = groupId;
}

_makeRequest = async (method: string) => {
const options: RequestInit = {
method,
headers: {
"content-type": "application/json",
accept: "application/json"
}
};

let url = "/api/lists";
if (method !== "GET") {
options.body = JSON.stringify({
groupId: this.groupId
});
} else {
url += `?groupId=${this.groupId}`;
}

return await fetch(url, options);
};

get = async () => {
return await this._makeRequest("GET");
};

create = async () => {
return await this._makeRequest("POST");
};
}
19 changes: 19 additions & 0 deletions src/lib/api/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,22 @@ export class UsersAPI {
return await this._makeRequest("GET", `?name=${search}`);
};
}

export class SystemUsersAPI {
_makeRequest = async (method: string, body: Record<string, any>) => {
const options: RequestInit = {
method,
headers: {
"content-type": "application/json",
accept: "application/json"
},
body: JSON.stringify(body)
};

return await fetch(`/api/users/public`, options);
};

create = async (username: string, name?: string) => {
return await this._makeRequest("POST", { username, name });
};
}
22 changes: 22 additions & 0 deletions src/lib/components/Alert.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script lang="ts">
type AlertType = "info" | "warning" | "error";
export let title: string | null = null;
export let type: AlertType;
const icon = type === "info" ? "information-circle" : "warning";
const variant = type === "info" ? "primary" : type === "warning" ? "warning" : "error";
</script>

<aside class="alert variant-ghost-{variant} mb-2">
<div class="alert-message flex flex-row items-center space-x-4 space-y-0">
<span><iconify-icon class="text-4xl" icon="ion:{icon}" /></span>
<div>
{#if title}
<span class="text-xl font-bold">{title}</span>
{/if}
<p>
<slot />
</p>
</div>
</div>
</aside>
3 changes: 2 additions & 1 deletion src/lib/components/BackButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"/forgot-password",
"/reset-password",
"/group-error",
/\/setup-wizard\/?.*/
/\/setup-wizard\/?.*/,
"/lists"
];
let documentTitle: string | undefined;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/TokenCopy.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { fade } from "svelte/transition";
export let url: string;
export let btnStyle = "btn-icon";
const dispatch = createEventDispatcher();
let copiedVisible = false;
Expand All @@ -23,7 +24,7 @@
</span>
<div class="flex flex-row items-center">
<button
class="btn btn-icon"
class="btn {btnStyle}"
type="button"
on:click={() => {
dispatch("copied");
Expand Down
21 changes: 21 additions & 0 deletions src/lib/components/admin/SettingsForm/ListMode.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import Alert from "$lib/components/Alert.svelte";
export let mode: ListMode;
export let disabled = false;
</script>

<div class="flex flex-col space-y-2">
<h2 class="h2">Wishlist Mode</h2>
{#if disabled}
<Alert type="info">
There are other members in this group, you cannot switch the mode until you remove all but one member.
</Alert>
{/if}
<select id="listMode" name="listMode" class="select w-fit min-w-64" bind:value={mode}>
<option value="standard">Wishlist</option>
{#if !disabled}
<option value="registry">Registry</option>
{/if}
</select>
</div>
Loading

0 comments on commit 5a433c6

Please sign in to comment.