mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Updating schema, adding admin pages, adding page for adding roles to admin on viewing user, block only admin to reach admin page.
This commit is contained in:
parent
c1aaad7f6c
commit
5e174c875f
31 changed files with 4148 additions and 335 deletions
13
drizzle/0004_glossy_enchantress.sql
Normal file
13
drizzle/0004_glossy_enchantress.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
ALTER TABLE "categories" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "collection_items" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "collections" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "expansions" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "external_ids" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "games" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "mechanics" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "publishers" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "roles" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "user_roles" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "users" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "wishlist_items" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "wishlists" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();
|
||||
2
drizzle/0005_light_captain_marvel.sql
Normal file
2
drizzle/0005_light_captain_marvel.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "roles" ALTER COLUMN "cuid" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "roles" ALTER COLUMN "name" SET NOT NULL;
|
||||
1513
drizzle/meta/0004_snapshot.json
Normal file
1513
drizzle/meta/0004_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
1513
drizzle/meta/0005_snapshot.json
Normal file
1513
drizzle/meta/0005_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -29,6 +29,20 @@
|
|||
"when": 1710268371021,
|
||||
"tag": "0003_mushy_madame_masque",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "5",
|
||||
"when": 1710277583673,
|
||||
"tag": "0004_glossy_enchantress",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "5",
|
||||
"when": 1710366724519,
|
||||
"tag": "0005_light_captain_marvel",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
22
package.json
22
package.json
|
|
@ -26,11 +26,11 @@
|
|||
"@playwright/test": "^1.42.1",
|
||||
"@resvg/resvg-js": "^2.6.0",
|
||||
"@sveltejs/adapter-auto": "^3.1.1",
|
||||
"@sveltejs/enhanced-img": "^0.1.8",
|
||||
"@sveltejs/kit": "^2.5.3",
|
||||
"@sveltejs/enhanced-img": "^0.1.9",
|
||||
"@sveltejs/kit": "^2.5.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^20.11.26",
|
||||
"@types/node": "^20.11.27",
|
||||
"@types/pg": "^8.11.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
|
|
@ -45,20 +45,21 @@
|
|||
"postcss": "^8.4.35",
|
||||
"postcss-import": "^16.0.1",
|
||||
"postcss-load-config": "^5.0.3",
|
||||
"postcss-preset-env": "^9.5.0",
|
||||
"postcss-preset-env": "^9.5.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.2",
|
||||
"sass": "^1.71.1",
|
||||
"sass": "^1.72.0",
|
||||
"satori": "^0.10.13",
|
||||
"satori-html": "^0.3.2",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.6",
|
||||
"svelte-check": "^3.6.7",
|
||||
"svelte-headless-table": "^0.18.2",
|
||||
"svelte-meta-tags": "^3.1.1",
|
||||
"svelte-preprocess": "^5.1.3",
|
||||
"svelte-sequential-preprocessor": "^2.0.1",
|
||||
"sveltekit-flash-message": "^2.4.4",
|
||||
"sveltekit-rate-limiter": "^0.4.3",
|
||||
"sveltekit-superforms": "^2.8.1",
|
||||
"sveltekit-superforms": "^2.9.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.6.1",
|
||||
|
|
@ -86,20 +87,21 @@
|
|||
"@sveltejs/adapter-vercel": "^5.1.1",
|
||||
"@types/feather-icons": "^4.29.4",
|
||||
"@vercel/og": "^0.5.20",
|
||||
"bits-ui": "^0.19.6",
|
||||
"bits-ui": "^0.19.7",
|
||||
"boardgamegeekclient": "^1.9.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cookie": "^0.6.0",
|
||||
"drizzle-orm": "^0.30.1",
|
||||
"drizzle-orm": "^0.30.2",
|
||||
"feather-icons": "^4.29.1",
|
||||
"formsnap": "^0.5.1",
|
||||
"html-entities": "^2.5.2",
|
||||
"iconify-icon": "^2.0.0",
|
||||
"just-capitalize": "^3.2.0",
|
||||
"just-kebab-case": "^4.2.0",
|
||||
"loader": "^2.1.1",
|
||||
"lucia": "3.1.1",
|
||||
"lucide-svelte": "^0.356.0",
|
||||
"lucide-svelte": "^0.358.0",
|
||||
"open-props": "^1.6.21",
|
||||
"oslo": "^1.1.3",
|
||||
"pg": "^8.11.3",
|
||||
|
|
|
|||
629
pnpm-lock.yaml
629
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
28
src/lib/components/ui/table/index.ts
Normal file
28
src/lib/components/ui/table/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import Root from "./table.svelte";
|
||||
import Body from "./table-body.svelte";
|
||||
import Caption from "./table-caption.svelte";
|
||||
import Cell from "./table-cell.svelte";
|
||||
import Footer from "./table-footer.svelte";
|
||||
import Head from "./table-head.svelte";
|
||||
import Header from "./table-header.svelte";
|
||||
import Row from "./table-row.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Body,
|
||||
Caption,
|
||||
Cell,
|
||||
Footer,
|
||||
Head,
|
||||
Header,
|
||||
Row,
|
||||
//
|
||||
Root as Table,
|
||||
Body as TableBody,
|
||||
Caption as TableCaption,
|
||||
Cell as TableCell,
|
||||
Footer as TableFooter,
|
||||
Head as TableHead,
|
||||
Header as TableHeader,
|
||||
Row as TableRow,
|
||||
};
|
||||
13
src/lib/components/ui/table/table-body.svelte
Normal file
13
src/lib/components/ui/table/table-body.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLTableSectionElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<tbody class={cn("[&_tr:last-child]:border-0", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</tbody>
|
||||
13
src/lib/components/ui/table/table-caption.svelte
Normal file
13
src/lib/components/ui/table/table-caption.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLTableCaptionElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<caption class={cn("mt-4 text-sm text-muted-foreground", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</caption>
|
||||
18
src/lib/components/ui/table/table-cell.svelte
Normal file
18
src/lib/components/ui/table/table-cell.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { HTMLTdAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLTdAttributes;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<td
|
||||
class={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<slot />
|
||||
</td>
|
||||
13
src/lib/components/ui/table/table-footer.svelte
Normal file
13
src/lib/components/ui/table/table-footer.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLTableSectionElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<tfoot class={cn("bg-primary font-medium text-primary-foreground", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</tfoot>
|
||||
19
src/lib/components/ui/table/table-head.svelte
Normal file
19
src/lib/components/ui/table/table-head.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { HTMLThAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLThAttributes;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<th
|
||||
class={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</th>
|
||||
14
src/lib/components/ui/table/table-header.svelte
Normal file
14
src/lib/components/ui/table/table-header.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLTableSectionElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<thead class={cn("[&_tr]:border-b", className)} {...$$restProps} on:click on:keydown>
|
||||
<slot />
|
||||
</thead>
|
||||
23
src/lib/components/ui/table/table-row.svelte
Normal file
23
src/lib/components/ui/table/table-row.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLTableRowElement> & {
|
||||
"data-state"?: unknown;
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<tr
|
||||
class={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<slot />
|
||||
</tr>
|
||||
15
src/lib/components/ui/table/table.svelte
Normal file
15
src/lib/components/ui/table/table.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { HTMLTableAttributes } from "svelte/elements";
|
||||
|
||||
type $$Props = HTMLTableAttributes;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class="w-full overflow-auto">
|
||||
<table class={cn("w-full caption-bottom text-sm", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -8,7 +8,7 @@ export const profileSchema = userSchema.pick({
|
|||
});
|
||||
|
||||
export const changeEmailSchema = userSchema.pick({
|
||||
email: true,
|
||||
email: true
|
||||
});
|
||||
|
||||
export const changeUserPasswordSchema = z
|
||||
|
|
@ -103,4 +103,12 @@ const checkPasswordStrength = async function (password: string, ctx: z.Refinemen
|
|||
path: ['password']
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const addRoleSchema = z.object({
|
||||
roles: z.array(z.string()).refine((value) => value.some((item) => item), {
|
||||
message: 'You have to select at least one item.'
|
||||
})
|
||||
});
|
||||
|
||||
export type AddRoleSchema = typeof addRoleSchema;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,32 @@
|
|||
import { redirect } from 'sveltekit-flash-message/server'
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
import db from '$lib/drizzle';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { user_roles } from '../../../../schema';
|
||||
|
||||
export async function load(event) {
|
||||
const { locals } = event;
|
||||
if (!locals?.user?.role?.includes('admin')) {
|
||||
if (!locals?.user) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
return {}
|
||||
};
|
||||
const { user } = locals;
|
||||
const userRoles = await db.query.user_roles.findMany({
|
||||
where: eq(user_roles.user_id, user.id),
|
||||
with: {
|
||||
role: {
|
||||
columns: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const containsAdminRole = userRoles.some((user_role) => user_role?.role?.name === 'admin');
|
||||
if (!userRoles?.length || !containsAdminRole) {
|
||||
console.log('Not an admin');
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,10 @@
|
|||
<slot />
|
||||
<h1>Do the admin stuff</h1>
|
||||
|
||||
<slot />
|
||||
|
||||
<style lang="postcss">
|
||||
:global(main) {
|
||||
margin: 0;
|
||||
max-width: 100vw;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { Button } from '$components/ui/button';
|
||||
</script>
|
||||
|
||||
<h1>At the admin page yo!</h1>
|
||||
|
||||
<Button href="/admin/users">Search for users</Button>
|
||||
23
src/routes/(app)/(protected)/admin/users/+page.server.ts
Normal file
23
src/routes/(app)/(protected)/admin/users/+page.server.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { redirect } from "sveltekit-flash-message/server";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { notSignedInMessage } from "$lib/flashMessages";
|
||||
import db from "$lib/drizzle";
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
|
||||
// TODO: Ensure admin user
|
||||
if (!event.locals.user) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const users = await db.query
|
||||
.users
|
||||
.findMany({
|
||||
limit: 10,
|
||||
offset: 0
|
||||
});
|
||||
|
||||
return {
|
||||
users
|
||||
};
|
||||
};
|
||||
10
src/routes/(app)/(protected)/admin/users/+page.svelte
Normal file
10
src/routes/(app)/(protected)/admin/users/+page.svelte
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import DataTable from './user-table.svelte';
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<h1>Users</h1>
|
||||
|
||||
<div class="container mx-auto py-10">
|
||||
<DataTable users={data?.users ?? []}/>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { eq, inArray, not } from 'drizzle-orm';
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
import db from '$lib/drizzle';
|
||||
import { roles, users } from '../../../../../../schema';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { params } = event;
|
||||
const { id } = params;
|
||||
|
||||
// TODO: Ensure admin user
|
||||
if (!event.locals.user) {
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const foundUser = await db.query.users.findFirst({
|
||||
where: eq(users.cuid, id),
|
||||
with: {
|
||||
user_roles: {
|
||||
with: {
|
||||
role: {
|
||||
columns: {
|
||||
name: true,
|
||||
cuid: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const currentRoleIds = foundUser?.user_roles?.map((user_role) => user_role?.role.cuid) || [];
|
||||
|
||||
console.log('currentRoleIds', currentRoleIds);
|
||||
|
||||
let availableRoles: { name: string; cuid: string }[] = [];
|
||||
if (currentRoleIds?.length > 0) {
|
||||
availableRoles = await db.query.roles.findMany({
|
||||
where: not(inArray(roles.cuid, currentRoleIds)),
|
||||
columns: {
|
||||
name: true,
|
||||
cuid: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
user: foundUser,
|
||||
availableRoles
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
addRole: async (event) => {
|
||||
const { params, request } = event;
|
||||
d;
|
||||
const data = await request.formData();
|
||||
console.log('data', data);
|
||||
|
||||
const roleCUID = data.get('value');
|
||||
const dbRole = await db.query.roles.findFirst({ where: eq(roles.cuid, roleCUID?.toString()) });
|
||||
console.log('dbRole', dbRole);
|
||||
}
|
||||
};
|
||||
33
src/routes/(app)/(protected)/admin/users/[id]/+page.svelte
Normal file
33
src/routes/(app)/(protected)/admin/users/[id]/+page.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import capitalize from 'just-capitalize';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
// import AddRolesForm from './add-roles-form.svelte';
|
||||
|
||||
export let data;
|
||||
const { user, availableRoles } = data;
|
||||
const { user_roles }: { user_roles: { role: { name: string, cuid: string } }[] } = user;
|
||||
</script>
|
||||
|
||||
<h1>User Details</h1>
|
||||
<p>Username {user?.username}</p>
|
||||
<p>Email Address: {user?.email || 'N/A'}</p>
|
||||
<p>First Name: {user?.first_name || 'N/A'}</p>
|
||||
<p>Last Name: {user?.last_name || 'N/A'}</p>
|
||||
|
||||
<h2>User Roles</h2>
|
||||
{#each user_roles as user_role}
|
||||
<p>{capitalize(user_role?.role?.name)}</p>
|
||||
{/each}
|
||||
|
||||
<h2>Roles Available to Assign</h2>
|
||||
<!--<AddRolesForm {availableRoles} />-->
|
||||
{#each availableRoles as role}
|
||||
<form action="?/addRole" method="POST" use:enhance>
|
||||
<div class="flex flex-row space-x-3 place-items-center mt-2">
|
||||
<input type="hidden" name="role" value={role?.cuid} />
|
||||
<Button type="submit">Add</Button>
|
||||
<p>{capitalize(role?.name)}</p>
|
||||
</div>
|
||||
</form>
|
||||
{/each}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts">
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Checkbox } from "$lib/components/ui/checkbox/index.js";
|
||||
import { addRoleSchema, type AddRoleSchema } from '$lib/validations/account';
|
||||
import {
|
||||
type SuperValidated,
|
||||
type Infer,
|
||||
superForm,
|
||||
} from "sveltekit-superforms";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
|
||||
export let availableRoles: { name: string; cuid: string }[] = [];
|
||||
const data: SuperValidated<Infer<AddRoleSchema>> = availableRoles;
|
||||
|
||||
const form = superForm(data, {
|
||||
validators: zodClient(addRoleSchema),
|
||||
// onUpdated: ({ form: f }) => {
|
||||
// if (f.valid) {
|
||||
// toast.success("You submitted" + JSON.stringify(f.data, null, 2));
|
||||
// } else {
|
||||
// toast.error("Please fix the errors in the form.");
|
||||
// }
|
||||
// }
|
||||
});
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
|
||||
function addRole(id: string) {
|
||||
$formData.roles = [...$formData.roles, id];
|
||||
}
|
||||
|
||||
function removeRole(id: string) {
|
||||
$formData.roles = $formData.roles.filter((i) => i !== id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<form action="/?/addMultipleRoles" method="POST" use:enhance>
|
||||
<Form.Fieldset {form} name="roles" class="space-y-0">
|
||||
<div class="mb-4">
|
||||
<Form.Legend class="text-base">Roles</Form.Legend>
|
||||
<Form.Description>
|
||||
Select the roles you want to add to the user.
|
||||
</Form.Description>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
{#each roles as item}
|
||||
{@const checked = $formData.roles.includes(item.cuid)}
|
||||
<div class="flex flex-row items-start space-x-3">
|
||||
<Form.Control let:attrs>
|
||||
<Checkbox
|
||||
{...attrs}
|
||||
{checked}
|
||||
onCheckedChange={(v) => {
|
||||
if (v) {
|
||||
addItem(item.id);
|
||||
} else {
|
||||
removeItem(item.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Form.Label class="text-sm font-normal">
|
||||
{item.label}
|
||||
</Form.Label>
|
||||
<input
|
||||
hidden
|
||||
type="checkbox"
|
||||
name={attrs.name}
|
||||
value={item.id}
|
||||
{checked}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
{/each}
|
||||
<Form.FieldErrors />
|
||||
</div>
|
||||
</Form.Fieldset>
|
||||
<Form.Button>Update display</Form.Button>
|
||||
{#if browser}
|
||||
<SuperDebug data={$formData} />
|
||||
{/if}
|
||||
</form>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
import Ellipsis from "lucide-svelte/icons/ellipsis";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { User } from 'lucide-svelte';
|
||||
|
||||
export let cuid: string;
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button
|
||||
variant="ghost"
|
||||
builders={[builder]}
|
||||
size="icon"
|
||||
class="relative h-8 w-8 p-0"
|
||||
>
|
||||
<span class="sr-only">Open menu</span>
|
||||
<Ellipsis class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>Actions</DropdownMenu.Label>
|
||||
<DropdownMenu.Item on:click={() => navigator.clipboard.writeText(cuid)}>
|
||||
Copy User ID
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<a href={`/admin/users/${cuid}`}>
|
||||
<DropdownMenu.Item>
|
||||
<User class="mr-2 h-4 w-4" />
|
||||
<span>View user</span>
|
||||
</DropdownMenu.Item>
|
||||
</a>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { Checkbox } from "$lib/components/ui/checkbox";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
export let checked: Writable<boolean>;
|
||||
</script>
|
||||
|
||||
<Checkbox bind:checked={$checked} />
|
||||
232
src/routes/(app)/(protected)/admin/users/user-table.svelte
Normal file
232
src/routes/(app)/(protected)/admin/users/user-table.svelte
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
createTable,
|
||||
Render,
|
||||
Subscribe,
|
||||
createRender,
|
||||
} from "svelte-headless-table";
|
||||
import {
|
||||
addPagination,
|
||||
addSortBy,
|
||||
addTableFilter,
|
||||
addHiddenColumns,
|
||||
addSelectedRows,
|
||||
} from "svelte-headless-table/plugins";
|
||||
import { readable } from "svelte/store";
|
||||
import ArrowUpDown from "lucide-svelte/icons/arrow-up-down";
|
||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||
import * as Table from "$lib/components/ui/table";
|
||||
import DataTableActions from "./user-table-actions.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import DataTableCheckbox from "./user-table-checkbox.svelte";
|
||||
import type { Users } from '../../../../../schema';
|
||||
|
||||
export let users: Users[] = [];
|
||||
|
||||
const table = createTable(readable(users), {
|
||||
page: addPagination(),
|
||||
sort: addSortBy({ disableMultiSort: true }),
|
||||
filter: addTableFilter({
|
||||
fn: ({ filterValue, value }) => value.includes(filterValue),
|
||||
}),
|
||||
hide: addHiddenColumns(),
|
||||
select: addSelectedRows(),
|
||||
});
|
||||
|
||||
const columns = table.createColumns([
|
||||
table.column({
|
||||
accessor: "cuid",
|
||||
header: (_, { pluginStates }) => {
|
||||
const { allPageRowsSelected } = pluginStates.select;
|
||||
return createRender(DataTableCheckbox, {
|
||||
checked: allPageRowsSelected,
|
||||
});
|
||||
},
|
||||
cell: ({ row }, { pluginStates }) => {
|
||||
const { getRowState } = pluginStates.select;
|
||||
const { isSelected } = getRowState(row);
|
||||
|
||||
return createRender(DataTableCheckbox, {
|
||||
checked: isSelected,
|
||||
});
|
||||
},
|
||||
plugins: {
|
||||
filter: {
|
||||
exclude: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
table.column({
|
||||
accessor: "username",
|
||||
header: "Username",
|
||||
}),
|
||||
table.column({
|
||||
accessor: "email",
|
||||
header: "Email",
|
||||
cell: ({ value }) => {
|
||||
return value ?? "N/A";
|
||||
}
|
||||
}),
|
||||
table.column({
|
||||
accessor: "first_name",
|
||||
header: "First Name",
|
||||
cell: ({ value }) => {
|
||||
return value && value.length > 0 ? value : "N/A";
|
||||
},
|
||||
plugins: {
|
||||
filter: {
|
||||
exclude: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
table.column({
|
||||
accessor: "last_name",
|
||||
header: "Last Name",
|
||||
cell: ({ value }) => {
|
||||
return value && value.length > 0 ? value : "N/A";
|
||||
},
|
||||
plugins: {
|
||||
filter: {
|
||||
exclude: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
table.column({
|
||||
accessor: ({ cuid }) => cuid,
|
||||
header: "",
|
||||
cell: ({ value }) => {
|
||||
return createRender(DataTableActions, { cuid: value });
|
||||
},
|
||||
plugins: {
|
||||
sort: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const {
|
||||
headerRows,
|
||||
pageRows,
|
||||
tableAttrs,
|
||||
tableBodyAttrs,
|
||||
pluginStates,
|
||||
flatColumns,
|
||||
rows,
|
||||
} = table.createViewModel(columns);
|
||||
|
||||
const { pageIndex, hasNextPage, hasPreviousPage } = pluginStates.page;
|
||||
const { filterValue } = pluginStates.filter;
|
||||
const { hiddenColumnIds } = pluginStates.hide;
|
||||
const { selectedDataIds } = pluginStates.select;
|
||||
|
||||
const ids = flatColumns.map((col) => col.id);
|
||||
let hideForId = Object.fromEntries(ids.map((id) => [id, true]));
|
||||
|
||||
$: $hiddenColumnIds = Object.entries(hideForId)
|
||||
.filter(([, hide]) => !hide)
|
||||
.map(([id]) => id);
|
||||
|
||||
const columnsToHide: string[] = [];
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center py-4">
|
||||
<Input
|
||||
class="max-w-sm"
|
||||
placeholder="Filter emails..."
|
||||
type="text"
|
||||
bind:value={$filterValue}
|
||||
/>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button variant="outline" class="ml-auto" builders={[builder]}>
|
||||
Columns <ChevronDown class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{#each flatColumns as col}
|
||||
{#if columnsToHide.includes(col.id)}
|
||||
<DropdownMenu.CheckboxItem bind:checked={hideForId[col.id]}>
|
||||
{col.header}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
{/if}
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
<div class="rounded-md border">
|
||||
<Table.Root {...$tableAttrs}>
|
||||
<Table.Header>
|
||||
{#each $headerRows as headerRow}
|
||||
<Subscribe rowAttrs={headerRow.attrs()}>
|
||||
<Table.Row>
|
||||
{#each headerRow.cells as cell (cell.id)}
|
||||
<Subscribe
|
||||
attrs={cell.attrs()}
|
||||
let:attrs
|
||||
props={cell.props()}
|
||||
let:props
|
||||
>
|
||||
<Table.Head {...attrs} class="[&:has([role=checkbox])]:pl-3">
|
||||
{#if cell.id === "email"}
|
||||
<Button variant="ghost" on:click={props.sort.toggle}>
|
||||
<Render of={cell.render()} />
|
||||
<ArrowUpDown class={"ml-2 h-4 w-4"} />
|
||||
</Button>
|
||||
{:else if cell.id === "username"}
|
||||
<Button variant="ghost" on:click={props.sort.toggle}>
|
||||
<Render of={cell.render()} />
|
||||
<ArrowUpDown class={"ml-2 h-4 w-4"} />
|
||||
</Button>
|
||||
{:else}
|
||||
<Render of={cell.render()} />
|
||||
{/if}
|
||||
</Table.Head>
|
||||
</Subscribe>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
</Subscribe>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body {...$tableBodyAttrs}>
|
||||
{#each $pageRows as row (row.id)}
|
||||
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
||||
<Table.Row
|
||||
{...rowAttrs}
|
||||
data-state={$selectedDataIds[row.id] && "selected"}
|
||||
>
|
||||
{#each row.cells as cell (cell.id)}
|
||||
<Subscribe attrs={cell.attrs()} let:attrs>
|
||||
<Table.Cell {...attrs} class="[&:has([role=checkbox])]:pl-3">
|
||||
<Render of={cell.render()} />
|
||||
</Table.Cell>
|
||||
</Subscribe>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
</Subscribe>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
<div class="flex items-center justify-end space-x-4 py-4">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
{Object.keys($selectedDataIds).length} of{" "}
|
||||
{$rows.length} row(s) selected.
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
on:click={() => ($pageIndex = $pageIndex - 1)}
|
||||
disabled={!$hasPreviousPage}>Previous</Button
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!$hasNextPage}
|
||||
on:click={() => ($pageIndex = $pageIndex + 1)}>Next</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -29,7 +29,12 @@ const signUpDefaults = {
|
|||
};
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
// redirect(302, '/waitlist', { type: 'error', message: 'Sign-up not yet available. Please add your email to the waitlist!' }, event);
|
||||
redirect(
|
||||
302,
|
||||
'/waitlist',
|
||||
{ type: 'error', message: 'Sign-up not yet available. Please add your email to the waitlist!' },
|
||||
event
|
||||
);
|
||||
|
||||
if (event.locals.user) {
|
||||
const message = { type: 'success', message: 'You are already signed in' } as const;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { tsvector } from './tsVector';
|
|||
// User Related Schemas
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -52,11 +52,12 @@ export const sessions = pgTable('sessions', {
|
|||
});
|
||||
|
||||
export const roles = pgTable('roles', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
name: text('name').unique()
|
||||
.$defaultFn(() => cuid2())
|
||||
.notNull(),
|
||||
name: text('name').unique().notNull()
|
||||
});
|
||||
|
||||
export type Roles = InferSelectModel<typeof roles>;
|
||||
|
|
@ -66,7 +67,7 @@ export const role_relations = relations(roles, ({ many }) => ({
|
|||
}));
|
||||
|
||||
export const user_roles = pgTable('user_roles', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -116,7 +117,7 @@ export const password_reset_token_relations = relations(password_reset_tokens, (
|
|||
}));
|
||||
|
||||
export const collections = pgTable('collections', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -135,7 +136,7 @@ export const collection_relations = relations(collections, ({ one }) => ({
|
|||
}));
|
||||
|
||||
export const collection_items = pgTable('collection_items', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -164,7 +165,7 @@ export const collection_item_relations = relations(collection_items, ({ one }) =
|
|||
}));
|
||||
|
||||
export const wishlists = pgTable('wishlists', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -185,7 +186,7 @@ export const wishlists_relations = relations(wishlists, ({ one }) => ({
|
|||
}));
|
||||
|
||||
export const wishlist_items = pgTable('wishlist_items', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -224,7 +225,7 @@ export const externalIdType = pgEnum('external_id_type', [
|
|||
]);
|
||||
|
||||
export const externalIds = pgTable('external_ids', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -237,7 +238,7 @@ export type ExternalIds = InferSelectModel<typeof externalIds>;
|
|||
export const games = pgTable(
|
||||
'games',
|
||||
{
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -301,7 +302,7 @@ export const gameRelations = relations(games, ({ many }) => ({
|
|||
}));
|
||||
|
||||
export const expansions = pgTable('expansions', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -329,7 +330,7 @@ export const expansion_relations = relations(expansions, ({ one }) => ({
|
|||
}));
|
||||
|
||||
export const publishers = pgTable('publishers', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -366,7 +367,7 @@ export const publishers_relations = relations(publishers, ({ many }) => ({
|
|||
}));
|
||||
|
||||
export const categories = pgTable('categories', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
@ -433,7 +434,7 @@ export const categories_relations = relations(categories, ({ many }) => ({
|
|||
}));
|
||||
|
||||
export const mechanics = pgTable('mechanics', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
|
|
|
|||
34
src/seed.ts
34
src/seed.ts
|
|
@ -1,5 +1,5 @@
|
|||
import 'dotenv/config';
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pg from 'pg';
|
||||
import * as schema from './schema';
|
||||
|
||||
|
|
@ -10,24 +10,32 @@ const pool = new pg.Pool({
|
|||
host: process.env.DATABASE_HOST,
|
||||
port: new Number(process.env.DATABASE_PORT).valueOf(),
|
||||
database: process.env.DATABASE_DB,
|
||||
ssl: process.env.DATABASE_HOST === 'localhost' ? false : true,
|
||||
ssl: process.env.DATABASE_HOST === 'localhost' ? false : true
|
||||
});
|
||||
|
||||
const db = drizzle(pool, { schema: schema });
|
||||
|
||||
const existingRoles = await db.query.roles.findMany();
|
||||
console.log('Existing roles', existingRoles);
|
||||
if (existingRoles.length === 0) {
|
||||
console.log('Creating roles ...');
|
||||
await db.insert(schema.roles).values([{
|
||||
name: 'admin'
|
||||
}, {
|
||||
name: 'user'
|
||||
}]);
|
||||
console.log('Roles created.');
|
||||
} else {
|
||||
console.log('Roles already exist. No action taken.');
|
||||
}
|
||||
console.log('Creating roles ...');
|
||||
await db
|
||||
.insert(schema.roles)
|
||||
.values([{ name: 'admin' }])
|
||||
.onConflictDoNothing();
|
||||
await db
|
||||
.insert(schema.roles)
|
||||
.values([{ name: 'user' }])
|
||||
.onConflictDoNothing();
|
||||
await db
|
||||
.insert(schema.roles)
|
||||
.values([{ name: 'editor' }])
|
||||
.onConflictDoNothing();
|
||||
console.log('Roles created.');
|
||||
await db
|
||||
.insert(schema.roles)
|
||||
.values([{ name: 'moderator' }])
|
||||
.onConflictDoNothing();
|
||||
console.log('Roles created.');
|
||||
|
||||
await pool.end();
|
||||
process.exit();
|
||||
|
|
|
|||
Loading…
Reference in a new issue