Merge pull request #27 from BradNut/development

Development merge
This commit is contained in:
Bradley Shellnut 2024-09-30 16:54:36 +00:00 committed by GitHub
commit f409abb1c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
206 changed files with 5776 additions and 12035 deletions

36
.env.example Normal file
View file

@ -0,0 +1,36 @@
# Private
ORIGIN=http://localhost:5173
NODE_ENV=development
DATABASE_USER='postgres'
DATABASE_PASSWORD='postgres'
DATABASE_HOST='localhost'
DATABASE_PORT=5432
DATABASE_DB='postgres'
REDIS_URL='redis://127.0.0.1:6379/0'
DB_MIGRATING='false'
DB_SEEDING='false'
ADMIN_USERNAME=
ADMIN_PASSWORD=
TWO_FACTOR_TIMEOUT=300000
# OAuth
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# Public
PUBLIC_SITE_NAME='Bored Game'
PUBLIC_SITE_URL='http://localhost:5173'
PUBLIC_UMAMI_DO_NOT_TRACK=true
PUBLIC_UMAMI_URL=
PUBLIC_UMAMI_ID=
# quick setting for key-combo only
SVELTE_INSPECTOR_TOGGLE=control-shift-i

View file

@ -1,3 +1,9 @@
{ {
"cSpell.words": ["iconify", "kickstarter", "lucide", "msrp", "pcss"] "cSpell.words": [
"iconify",
"kickstarter",
"lucide",
"msrp",
"pcss"
]
} }

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License Copyright (c) 2024 Bradley Shellnut
Permission is hereby granted,
free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -3,12 +3,12 @@
"style": "default", "style": "default",
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "src/app.postcss", "css": "src/lib/styles/app.pcss",
"baseColor": "slate" "baseColor": "slate"
}, },
"aliases": { "aliases": {
"components": "$lib/components", "components": "$lib/components",
"utils": "$lib/utils" "utils": "$lib/utils/ui"
}, },
"typescript": true "typescript": true
} }

View file

@ -1,6 +1,6 @@
import 'dotenv/config' import 'dotenv/config'
import env from './src/lib/server/api/common/env'
import { defineConfig } from 'drizzle-kit' import { defineConfig } from 'drizzle-kit'
import env from './src/env'
export default defineConfig({ export default defineConfig({
dialect: 'postgresql', dialect: 'postgresql',

View file

@ -1,20 +1,20 @@
import { error, json } from '@sveltejs/kit'; import { getGame } from '$lib/utils/db/gameUtils.js'
import { getGame } from '$lib/utils/db/gameUtils.js'; import { error, json } from '@sveltejs/kit'
export const GET = async ({ locals, params }) => { export const GET = async ({ locals, params }) => {
const game_id = Number(params.id).valueOf(); const game_id = Number(params.id).valueOf()
// TODO: Debounce excessive calls and possibly throttle // TODO: Debounce excessive calls and possibly throttle
if (isNaN(game_id) || !isFinite(game_id)) { if (isNaN(game_id) || !isFinite(game_id)) {
error(400, { message: 'Invalid game id' }); error(400, { message: 'Invalid game id' })
} }
try { try {
return json(await getGame(locals, params.id)); return json(await getGame(locals, params.id))
} catch (e) { } catch (e) {
console.error(e); console.error(e)
return new Response('Could not get games', { return new Response('Could not get gamesTable', {
status: 500 status: 500,
}); })
} }
} }

View file

@ -1,44 +1,34 @@
import { error, json } from '@sveltejs/kit'; import { games } from '$db/schema'
import db from '../../../../db'; import { FilterSchema, PaginationSchema, SearchSchema, SortSchema } from '$lib/validations/zod-schemas'
import { asc, desc, eq, ilike, or } from 'drizzle-orm'; import { error, json } from '@sveltejs/kit'
import { games } from '$db/schema'; import { asc, desc, eq, ilike, or } from 'drizzle-orm'
import kebabCase from 'just-kebab-case'; import kebabCase from 'just-kebab-case'
import { import db from '../../../../db'
FilterSchema,
PaginationSchema,
SearchSchema,
SortSchema,
} from '$lib/validations/zod-schemas';
// Search a user's collection // Search a user's collection
export const GET = async ({ url, locals }) => { export const GET = async ({ url, locals }) => {
const searchParams = Object.fromEntries(url.searchParams); const searchParams = Object.fromEntries(url.searchParams)
const searchGames = PaginationSchema.merge(FilterSchema) const searchGames = PaginationSchema.merge(FilterSchema).merge(SortSchema).merge(SearchSchema).parse(searchParams)
.merge(SortSchema)
.merge(SearchSchema)
.parse(searchParams);
if (searchGames.status !== 'success') { if (searchGames.status !== 'success') {
error(400, 'Invalid request'); error(400, 'Invalid request')
} }
const q = searchParams?.q?.trim() || ''; const q = searchParams?.q?.trim() || ''
const limit = parseInt(searchParams?.limit) || 10; const limit = parseInt(searchParams?.limit) || 10
const skip = parseInt(searchParams?.skip) || 0; const skip = parseInt(searchParams?.skip) || 0
const order: OrderDirection = searchParams?.order === 'desc' ? 'desc' : 'asc'; const order: OrderDirection = searchParams?.order === 'desc' ? 'desc' : 'asc'
const exact = searchParams?.exact === 'true'; const exact = searchParams?.exact === 'true'
let orderBy = searchParams?.orderBy || 'slug'; let orderBy = searchParams?.orderBy || 'slug'
if (orderBy === 'name') { if (orderBy === 'name') {
orderBy = 'slug'; orderBy = 'slug'
} }
console.log( console.log(`q: ${q}, limit: ${limit}, skip: ${skip}, order: ${order}, exact: ${exact}, orderBy: ${orderBy}`)
`q: ${q}, limit: ${limit}, skip: ${skip}, order: ${order}, exact: ${exact}, orderBy: ${orderBy}`, console.log(exact)
);
console.log(exact);
if (exact) { if (exact) {
console.log('Exact Search API'); console.log('Exact Search API')
const game = await db.query.games.findFirst({ const game = await db.query.games.findFirst({
where: eq(games.name, q), where: eq(games.name, q),
columns: { columns: {
@ -47,14 +37,14 @@ export const GET = async ({ url, locals }) => {
slug: true, slug: true,
thumb_url: true, thumb_url: true,
}, },
}); })
if (!game) { if (!game) {
error(404, { message: 'No games found' }); error(404, { message: 'No gamesTable found' })
} }
const foundGames = [game]; const foundGames = [game]
console.log('Games found in Exact Search API', JSON.stringify(foundGames, null, 2)); console.log('Games found in Exact Search API', JSON.stringify(foundGames, null, 2))
return json(foundGames); return json(foundGames)
} else { } else {
const foundGames = const foundGames =
(await db (await db
@ -68,37 +58,37 @@ export const GET = async ({ url, locals }) => {
.where(or(ilike(games.name, `%${q}%`), ilike(games.slug, `%${kebabCase(q)}%`))) .where(or(ilike(games.name, `%${q}%`), ilike(games.slug, `%${kebabCase(q)}%`)))
.orderBy(getOrderDirection(order)(getOrderBy(orderBy))) .orderBy(getOrderDirection(order)(getOrderBy(orderBy)))
.offset(skip) .offset(skip)
.limit(limit)) || []; .limit(limit)) || []
// const foundGames = await db.select({ // const foundGames = await db.select({
// id: games.id, // id: gamesTable.id,
// name: games.name, // name: gamesTable.name,
// slug: games.slug, // slug: gamesTable.slug,
// thumb_url: games.thumb_url // thumb_url: gamesTable.thumb_url
// }) // })
// .from(games) // .from(gamesTable)
// .where(sql`to_tsvector('simple', ${games.name}) || to_tsvector('simple', ${games.slug}) @@ to_tsquery('simple', ${q})`) // .where(sql`to_tsvector('simple', ${gamesTable.name}) || to_tsvector('simple', ${gamesTable.slug}) @@ to_tsquery('simple', ${q})`)
// .orderBy(sql`${orderBy} ${order}`).offset(skip).limit(limit) || []; // .orderBy(sql`${orderBy} ${order}`).offset(skip).limit(limit) || [];
if (foundGames.length === 0) { if (foundGames.length === 0) {
error(404, { message: 'No games found' }); error(404, { message: 'No gamesTable found' })
}
console.log('Games found in Search API', JSON.stringify(foundGames, null, 2))
return json(foundGames)
} }
console.log('Games found in Search API', JSON.stringify(foundGames, null, 2));
return json(foundGames);
} }
};
type OrderDirection = 'asc' | 'desc'; type OrderDirection = 'asc' | 'desc'
const getOrderDirection = (direction: OrderDirection) => { const getOrderDirection = (direction: OrderDirection) => {
return direction === 'asc' ? asc : desc; return direction === 'asc' ? asc : desc
}; }
const getOrderBy = (orderBy: string) => { const getOrderBy = (orderBy: string) => {
switch (orderBy) { switch (orderBy) {
case 'name': case 'name':
return games.name; return games.name
case 'slug': case 'slug':
return games.slug; return games.slug
default: default:
return games.slug; return games.slug
}
} }
};

View file

@ -1,19 +1,19 @@
import { getPublisher, updatePublisher } from '$lib/utils/db/publisherUtils.js'; import type { Publishers } from '$db/schema'
import type { Publishers } from '$db/schema'; import { getPublisher, updatePublisher } from '$lib/utils/db/publisherUtils.js'
export async function GET({ locals, params }) { export async function GET({ locals, params }) {
try { try {
return await getPublisher(locals, params.id); return await getPublisher(locals, params.id)
} catch (e) { } catch (e) {
console.error(e); console.error(e)
return new Response('Could not get publishers', { return new Response('Could not get publishersTable', {
status: 500, status: 500,
}); })
} }
} }
export async function PUT({ locals, params, request }) { export async function PUT({ locals, params, request }) {
const data: Publishers = await request.json(); const data: Publishers = await request.json()
const publisherId = params.id; const publisherId = params.id
return await updatePublisher(locals, data, publisherId); return await updatePublisher(locals, data, publisherId)
} }

View file

@ -27,56 +27,54 @@
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@melt-ui/pp": "^0.3.2", "@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0", "@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.47.1",
"@sveltejs/adapter-auto": "^3.2.4", "@sveltejs/adapter-auto": "^3.2.5",
"@sveltejs/enhanced-img": "^0.3.4", "@sveltejs/enhanced-img": "^0.3.8",
"@sveltejs/kit": "^2.5.25", "@sveltejs/kit": "^2.6.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2", "@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^20.16.3", "@types/node": "^20.16.10",
"@types/pg": "^8.11.8", "@types/pg": "^8.11.10",
"@types/qrcode": "^1.5.5",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
"arctic": "^1.9.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"drizzle-kit": "^0.23.2", "drizzle-kit": "^0.23.2",
"eslint": "^8.57.0", "eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.43.0", "eslint-plugin-svelte": "2.36.0-next.13",
"just-clone": "^6.2.0", "just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0", "just-debounce-it": "^3.2.0",
"lucia": "3.2.0", "lucia": "3.2.0",
"lucide-svelte": "^0.408.0", "lucide-svelte": "^0.408.0",
"nodemailer": "^6.9.15", "nodemailer": "^6.9.15",
"postcss": "^8.4.44", "postcss": "^8.4.47",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-load-config": "^5.1.0", "postcss-load-config": "^5.1.0",
"postcss-preset-env": "^9.6.0", "postcss-preset-env": "^9.6.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.2.7",
"sass": "^1.77.8",
"satori": "^0.10.14",
"satori-html": "^0.3.2",
"svelte": "5.0.0-next.175", "svelte": "5.0.0-next.175",
"svelte-check": "^3.8.6", "svelte-check": "^3.8.6",
"svelte-headless-table": "^0.18.2", "svelte-headless-table": "^0.18.2",
"svelte-meta-tags": "^3.1.4", "svelte-meta-tags": "^3.1.4",
"svelte-preprocess": "^6.0.2", "svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.1", "svelte-sequential-preprocessor": "^2.0.2",
"sveltekit-flash-message": "^2.4.4", "sveltekit-flash-message": "^2.4.4",
"sveltekit-rate-limiter": "^0.5.2", "sveltekit-superforms": "^2.18.1",
"sveltekit-superforms": "^2.17.0", "tailwindcss": "^3.4.13",
"tailwindcss": "^3.4.10",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tslib": "^2.7.0", "tslib": "^2.7.0",
"tsx": "^4.19.0", "tsx": "^4.19.1",
"typescript": "^5.5.4", "typescript": "^5.6.2",
"vite": "^5.4.3", "vite": "^5.4.8",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^5.0.14", "@fontsource/fira-mono": "^5.1.0",
"@hono/swagger-ui": "^0.4.1", "@hono/swagger-ui": "^0.4.1",
"@hono/zod-openapi": "^0.15.3", "@hono/zod-openapi": "^0.15.3",
"@hono/zod-validator": "^0.2.2", "@hono/zod-validator": "^0.2.2",
@ -86,16 +84,20 @@
"@lucia-auth/adapter-drizzle": "^1.1.0", "@lucia-auth/adapter-drizzle": "^1.1.0",
"@lukeed/uuid": "^2.0.1", "@lukeed/uuid": "^2.0.1",
"@neondatabase/serverless": "^0.9.5", "@neondatabase/serverless": "^0.9.5",
"@node-rs/argon2": "^1.8.3",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@oslojs/jwt": "^0.2.0",
"@oslojs/oauth2": "^0.5.0",
"@oslojs/otp": "^1.0.0",
"@oslojs/webauthn": "^1.0.0",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@resvg/resvg-js": "^2.6.2", "@sveltejs/adapter-node": "^5.2.5",
"@sveltejs/adapter-node": "^5.2.2", "@sveltejs/adapter-vercel": "^5.4.4",
"@sveltejs/adapter-vercel": "^5.4.3",
"@types/feather-icons": "^4.29.4", "@types/feather-icons": "^4.29.4",
"@vercel/og": "^0.5.20",
"arctic": "^1.9.2",
"bits-ui": "^0.21.13", "bits-ui": "^0.21.13",
"boardgamegeekclient": "^1.9.1", "boardgamegeekclient": "^1.9.1",
"bullmq": "^5.12.13", "bullmq": "^5.14.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookie": "^0.6.0", "cookie": "^0.6.0",
@ -106,7 +108,7 @@
"feather-icons": "^4.29.2", "feather-icons": "^4.29.2",
"formsnap": "^1.0.1", "formsnap": "^1.0.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"hono": "^4.5.11", "hono": "^4.6.3",
"hono-rate-limiter": "^0.4.0", "hono-rate-limiter": "^0.4.0",
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"iconify-icon": "^2.1.0", "iconify-icon": "^2.1.0",
@ -116,7 +118,7 @@
"loader": "^2.1.1", "loader": "^2.1.1",
"open-props": "^1.7.6", "open-props": "^1.7.6",
"oslo": "^1.2.1", "oslo": "^1.2.1",
"pg": "^8.12.0", "pg": "^8.13.0",
"postgres": "^3.4.4", "postgres": "^3.4.4",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"radix-svelte": "^0.9.0", "radix-svelte": "^0.9.0",
@ -128,6 +130,6 @@
"tailwind-variants": "^0.2.1", "tailwind-variants": "^0.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tsyringe": "^4.8.0", "tsyringe": "^4.8.0",
"zod-to-json-schema": "^3.23.2" "zod-to-json-schema": "^3.23.3"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,15 @@
<script lang="ts"> <script lang="ts">
import { applyAction, enhance } from '$app/forms'; import { applyAction, enhance } from '$app/forms'
import toast from 'svelte-french-toast'; import { invalidateAll } from '$app/navigation'
import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte'; import Logo from '$components/logo.svelte'
import * as DropdownMenu from '$components/ui/dropdown-menu'; import * as Avatar from '$components/ui/avatar'
import * as Avatar from '$components/ui/avatar'; import * as DropdownMenu from '$components/ui/dropdown-menu'
import { invalidateAll } from '$app/navigation'; import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte'
import Logo from '$components/logo.svelte'; import toast from 'svelte-french-toast'
import type { Users } from '$db/schema';
type HeaderProps = { let { user = null } = $props()
user: Users | null;
};
let { user = null }: HeaderProps = $props(); let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)');
</script> </script>
<header> <header>
@ -28,6 +23,15 @@
</div> </div>
<nav> <nav>
{#if user} {#if user}
{@render userDropdown()}
{:else}
<a href="/login"> <span class="flex-auto">Login</span></a>
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
{/if}
</nav>
</header>
{#snippet userDropdown()}
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<Avatar.Root asChild> <Avatar.Root asChild>
@ -40,10 +44,10 @@
<DropdownMenu.Group> <DropdownMenu.Group>
<DropdownMenu.Label>Account</DropdownMenu.Label> <DropdownMenu.Label>Account</DropdownMenu.Label>
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<a href="/profile"> <a href="/settings">
<DropdownMenu.Item> <DropdownMenu.Item>
<User class="mr-2 h-4 w-4" /> <Settings class="mr-2 h-4 w-4" />
<span>Profile</span> <span>Settings</span>
</DropdownMenu.Item> </DropdownMenu.Item>
</a> </a>
<a href="/collections"> <a href="/collections">
@ -90,12 +94,7 @@
</DropdownMenu.Group> </DropdownMenu.Group>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
{:else} {/snippet}
<a href="/login"> <span class="flex-auto">Login</span></a>
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
{/if}
</nav>
</header>
<style lang="postcss"> <style lang="postcss">
header { header {

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { cn } from "$lib/utils"; import { type Variant, badgeVariants } from "./index.js";
import { badgeVariants, type Variant } from "."; import { cn } from "$lib/utils/ui.js";
let className: string | undefined | null = undefined; let className: string | undefined | null = undefined;
export let href: string | undefined = undefined; export let href: string | undefined = undefined;

View file

@ -1,22 +1,21 @@
import { tv, type VariantProps } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export { default as Badge } from "./badge.svelte"; export { default as Badge } from "./badge.svelte";
export const badgeVariants = tv({ export const badgeVariants = tv({
base: "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none select-none focus:ring-2 focus:ring-ring focus:ring-offset-2", base: "focus:ring-ring inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
variants: { variants: {
variant: { variant: {
default: default: "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
secondary: secondary:
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground", "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
destructive: destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground", "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
outline: "text-foreground" outline: "text-foreground",
} },
}, },
defaultVariants: { defaultVariants: {
variant: "default" variant: "default",
} },
}); });
export type Variant = VariantProps<typeof badgeVariants>["variant"]; export type Variant = VariantProps<typeof badgeVariants>["variant"];

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements"; import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js"; import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLInputAttributes; type $$Props = HTMLInputAttributes;
type $$Events = InputEvents; type $$Events = InputEvents;

View file

@ -2,12 +2,12 @@ import { type VariantProps, tv } from "tailwind-variants";
import Root from "./toggle.svelte"; import Root from "./toggle.svelte";
export const toggleVariants = tv({ export const toggleVariants = tv({
base: "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", base: "ring-offset-background hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
outline: outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent",
}, },
size: { size: {
default: "h-10 px-3", default: "h-10 px-3",

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Toggle as TogglePrimitive } from "bits-ui"; import { Toggle as TogglePrimitive } from "bits-ui";
import { type Size, type Variant, toggleVariants } from "./index.js"; import { type Size, type Variant, toggleVariants } from "./index.js";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils/ui.js";
type $$Props = TogglePrimitive.Props & { type $$Props = TogglePrimitive.Props & {
variant?: Variant; variant?: Variant;

View file

@ -1,36 +1,37 @@
<script lang="ts"> <script lang="ts">
import { navigating } from '$app/stores'; import { onNavigate } from '$app/navigation'
import { onNavigate } from '$app/navigation'; import { navigating } from '$app/stores'
let visible = false;
let progress = 0; let visible = false
let load_durations: number[] = []; let progress = 0
$: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length; let load_durations: number[] = []
const increment = 1; $: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length
const increment = 1
onNavigate((navigation) => { onNavigate((navigation) => {
const typical_load_time = average_load || 200; //ms const typical_load_time = average_load || 200 //ms
const frequency = typical_load_time / 100; const frequency = typical_load_time / 100
let start = performance.now(); let start = performance.now()
// Start the progress bar // Start the progress bar
visible = true; visible = true
progress = 0; progress = 0
const interval = setInterval(() => { const interval = setInterval(() => {
// Increment the progress bar // Increment the progress bar
progress += increment; progress += increment
}, frequency); }, frequency)
// Resolve the promise when the page is done loading // Resolve the promise when the page is done loading
$navigating?.complete.then(() => { $navigating?.complete.then(() => {
progress = 100; // Fill out the progress bar progress = 100 // Fill out the progress bar
clearInterval(interval); clearInterval(interval)
// after 100 ms hide the progress bar // after 100 ms hide the progress bar
setTimeout(() => { setTimeout(() => {
visible = false; visible = false
}, 500); }, 500)
// Log how long that one took // Log how long that one took
const end = performance.now(); const end = performance.now()
const duration = end - start; const duration = end - start
load_durations = [...load_durations, duration]; load_durations = [...load_durations, duration]
}); })
}); })
</script> </script>
<div class="progress" class:visible style:--progress={progress}> <div class="progress" class:visible style:--progress={progress}>

View file

@ -1,45 +0,0 @@
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import { html as toReactNode } from 'satori-html';
import { dev } from '$app/environment';
import { read } from '$app/server';
// we use a Vite plugin to turn this import into the result of fs.readFileSync during build
import firaSansSemiBold from '$lib/fonts/FiraSans-Bold.ttf';
const fontData = read(firaSansSemiBold).arrayBuffer();
export async function componentToPng(component,
props: Record<string, string | undefined>,
height: number, width: number) {
const result = component.render(props);
const markup = toReactNode(`${result.html}<style lang="css">${result.css.code}</style>`);
const svg = await satori(markup, {
fonts: [
{
name: 'Fira Sans',
data: await fontData,
style: 'normal'
}
],
height: +height,
width: +width
});
const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: +width
}
});
const image = resvg.render();
return new Response(image.asPng(), {
headers: {
'content-type': 'image/png',
'cache-control': dev ? 'no-cache, no-store' : 'public, immutable, no-transform, max-age=86400'
}
});
}

View file

@ -0,0 +1,36 @@
import env from './env'
import type { Config } from './types/config'
const isPreview = process.env.VERCEL_ENV === 'preview' || process.env.VERCEL_ENV === 'development'
let domain: string
if (process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production') {
domain = 'boredgame.vercel.app'
} else if (isPreview && process.env.VERCEL_BRANCH_URL !== undefined) {
domain = process.env.VERCEL_BRANCH_URL
} else {
domain = 'localhost'
}
// export const config = { ...env, isProduction: process.env.NODE_ENV === 'production'
// || process.env.VERCEL_ENV === 'production', domain };
export const config: Config = {
isProduction: process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production',
domain,
api: {
origin: env.ORIGIN,
},
redis: {
url: env.REDIS_URL,
},
postgres: {
user: env.DATABASE_USER,
password: env.DATABASE_PASSWORD,
host: env.DATABASE_HOST,
port: env.DATABASE_PORT,
database: env.DATABASE_DB,
ssl: env.DATABASE_HOST !== 'localhost',
max: env.DB_MIGRATING || env.DB_SEEDING ? 1 : undefined,
},
}

View file

@ -10,8 +10,6 @@ const stringBoolean = z.coerce
.default('false') .default('false')
const EnvSchema = z.object({ const EnvSchema = z.object({
ADMIN_USERNAME: z.string(),
ADMIN_PASSWORD: z.string(),
DATABASE_USER: z.string(), DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(), DATABASE_PASSWORD: z.string(),
DATABASE_HOST: z.string(), DATABASE_HOST: z.string(),
@ -19,6 +17,10 @@ const EnvSchema = z.object({
DATABASE_DB: z.string(), DATABASE_DB: z.string(),
DB_MIGRATING: stringBoolean, DB_MIGRATING: stringBoolean,
DB_SEEDING: stringBoolean, DB_SEEDING: stringBoolean,
GITHUB_CLIENT_ID: z.string(),
GITHUB_CLIENT_SECRET: z.string(),
GOOGLE_CLIENT_ID: z.string(),
GOOGLE_CLIENT_SECRET: z.string(),
NODE_ENV: z.string().default('development'), NODE_ENV: z.string().default('development'),
ORIGIN: z.string(), ORIGIN: z.string(),
PUBLIC_SITE_NAME: z.string(), PUBLIC_SITE_NAME: z.string(),

View file

@ -1,26 +1,26 @@
import { StatusCodes } from '$lib/constants/status-codes'; import { StatusCodes } from '$lib/constants/status-codes'
import { HTTPException } from 'hono/http-exception'; import { HTTPException } from 'hono/http-exception'
export function TooManyRequests(message: string = 'Too many requests') { export function TooManyRequests(message = 'Too many requests') {
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message }); return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message })
} }
export function Forbidden(message: string = 'Forbidden') { export function Forbidden(message = 'Forbidden') {
return new HTTPException(StatusCodes.FORBIDDEN, { message }); return new HTTPException(StatusCodes.FORBIDDEN, { message })
} }
export function Unauthorized(message: string = 'Unauthorized') { export function Unauthorized(message = 'Unauthorized') {
return new HTTPException(StatusCodes.UNAUTHORIZED, { message }); return new HTTPException(StatusCodes.UNAUTHORIZED, { message })
} }
export function NotFound(message: string = 'Not Found') { export function NotFound(message = 'Not Found') {
return new HTTPException(StatusCodes.NOT_FOUND, { message }); return new HTTPException(StatusCodes.NOT_FOUND, { message })
} }
export function BadRequest(message: string = 'Bad Request') { export function BadRequest(message = 'Bad Request') {
return new HTTPException(StatusCodes.BAD_REQUEST, { message }); return new HTTPException(StatusCodes.BAD_REQUEST, { message })
} }
export function InternalError(message: string = 'Internal Error') { export function InternalError(message = 'Internal Error') {
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message }); return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message })
} }

View file

@ -1,8 +0,0 @@
import { Hono } from 'hono'
import type { BlankSchema } from 'hono/types'
import type { HonoTypes } from '../../types'
export interface Controller {
controller: Hono<HonoTypes, BlankSchema, '/'>
routes(): any
}

View file

@ -1,5 +0,0 @@
import type { DatabaseProvider } from '$lib/server/api/providers/database.provider'
export interface Repository {
trxHost(trx: DatabaseProvider): any
}

View file

@ -0,0 +1,3 @@
export abstract class AsyncService {
async init(): Promise<void> {}
}

View file

@ -0,0 +1,33 @@
export interface Config {
isProduction: boolean
domain: string
api: ApiConfig
// storage: StorageConfig
redis: RedisConfig
postgres: PostgresConfig
}
interface ApiConfig {
origin: string
}
// interface StorageConfig {
// accessKey: string
// secretKey: string
// bucket: string
// url: string
// }
interface RedisConfig {
url: string
}
interface PostgresConfig {
user: string
password: string
host: string
port: number
database: string
ssl: boolean
max: number | undefined
}

View file

@ -0,0 +1,11 @@
import { Hono } from 'hono'
import type { BlankSchema } from 'hono/types'
import type { HonoTypes } from './hono'
export abstract class Controller {
protected readonly controller: Hono<HonoTypes, BlankSchema, '/'>
constructor() {
this.controller = new Hono()
}
abstract routes(): Hono<HonoTypes, BlankSchema, '/'>
}

View file

@ -0,0 +1,11 @@
export type OAuthUser = {
sub: string;
given_name?: string;
family_name?: string;
picture?: string;
username: string;
email?: string;
email_verified?: boolean;
}
export type OAuthProviders = 'github' | 'google' | 'apple'

View file

@ -1,15 +0,0 @@
import env from '../../../../env';
const isPreview = process.env.VERCEL_ENV === 'preview' || process.env.VERCEL_ENV === 'development';
let domain: string;
if (process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production') {
domain = 'boredgame.vercel.app';
} else if (isPreview && process.env.VERCEL_BRANCH_URL !== undefined) {
domain = process.env.VERCEL_BRANCH_URL;
} else {
domain = 'localhost';
}
export const config = { ...env, isProduction: process.env.NODE_ENV === 'production'
|| process.env.VERCEL_ENV === 'production', domain };

View file

@ -1,16 +1,14 @@
import 'reflect-metadata' import 'reflect-metadata'
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface' import { Controller } from '$lib/server/api/common/types/controller'
import { CollectionsService } from '$lib/server/api/services/collections.service' import { CollectionsService } from '$lib/server/api/services/collections.service'
import { Hono } from 'hono'
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware' import { requireAuth } from '../middleware/require-auth.middleware'
import type { HonoTypes } from '../types'
@injectable() @injectable()
export class CollectionController implements Controller { export class CollectionController extends Controller {
controller = new Hono<HonoTypes>() constructor(@inject(CollectionsService) private readonly collectionsService: CollectionsService) {
super()
constructor(@inject(CollectionsService) private readonly collectionsService: CollectionsService) {} }
routes() { routes() {
return this.controller return this.controller
@ -20,6 +18,11 @@ export class CollectionController implements Controller {
console.log('collections service', collections) console.log('collections service', collections)
return c.json({ collections }) return c.json({ collections })
}) })
.get('/count', requireAuth, async (c) => {
const user = c.var.user
const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id)
return c.json({ collections })
})
.get('/:cuid', requireAuth, async (c) => { .get('/:cuid', requireAuth, async (c) => {
const cuid = c.req.param('cuid') const cuid = c.req.param('cuid')
const collection = await this.collectionsService.findOneByCuid(cuid) const collection = await this.collectionsService.findOneByCuid(cuid)

View file

@ -1,26 +1,27 @@
import { StatusCodes } from '$lib/constants/status-codes' import { StatusCodes } from '$lib/constants/status-codes'
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface' import { Controller } from '$lib/server/api/common/types/controller'
import { changePasswordDto } from '$lib/server/api/dtos/change-password.dto'
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto' import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto' import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto'
import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto' import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware' import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import { LuciaProvider } from '$lib/server/api/providers/lucia.provider'
import { IamService } from '$lib/server/api/services/iam.service' import { IamService } from '$lib/server/api/services/iam.service'
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service'
import { LuciaService } from '$lib/server/api/services/lucia.service'
import { zValidator } from '@hono/zod-validator' import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { setCookie } from 'hono/cookie' import { setCookie } from 'hono/cookie'
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware' import { requireAuth } from '../middleware/require-auth.middleware'
import type { HonoTypes } from '../types'
@injectable() @injectable()
export class IamController implements Controller { export class IamController extends Controller {
controller = new Hono<HonoTypes>()
constructor( constructor(
@inject(IamService) private readonly iamService: IamService, @inject(IamService) private readonly iamService: IamService,
@inject(LuciaProvider) private lucia: LuciaProvider, @inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
) {} @inject(LuciaService) private luciaService: LuciaService,
) {
super()
}
routes() { routes() {
return this.controller return this.controller
@ -47,6 +48,32 @@ export class IamController implements Controller {
} }
return c.json({}, StatusCodes.OK) return c.json({}, StatusCodes.OK)
}) })
.put('/update/password', requireAuth, zValidator('json', changePasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const user = c.var.user
const { password, confirm_password } = c.req.valid('json')
if (password !== confirm_password) {
return c.json('Passwords do not match', StatusCodes.BAD_REQUEST)
}
try {
await this.iamService.updatePassword(user.id, { password, confirm_password })
await this.luciaService.lucia.invalidateUserSessions(user.id)
await this.loginRequestService.createUserSession(user.id, c.req, undefined)
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie()
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge,
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
})
return c.json({ status: 'success' })
} catch (error) {
console.error('Error updating password', error)
return c.json('Error updating password', StatusCodes.BAD_REQUEST)
}
})
.post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { .post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const user = c.var.user const user = c.var.user
const { email } = c.req.valid('json') const { email } = c.req.valid('json')
@ -59,7 +86,7 @@ export class IamController implements Controller {
.post('/logout', requireAuth, async (c) => { .post('/logout', requireAuth, async (c) => {
const sessionId = c.var.session.id const sessionId = c.var.session.id
await this.iamService.logout(sessionId) await this.iamService.logout(sessionId)
const sessionCookie = this.lucia.createBlankSessionCookie() const sessionCookie = this.luciaService.lucia.createBlankSessionCookie()
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge, maxAge: sessionCookie.attributes.maxAge,

View file

@ -1,30 +1,28 @@
import 'reflect-metadata' import 'reflect-metadata'
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface' import { Controller } from '$lib/server/api/common/types/controller'
import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto' import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto'
import { LuciaProvider } from '$lib/server/api/providers/lucia.provider' import { LuciaService } from '$lib/server/api/services/lucia.service'
import { zValidator } from '@hono/zod-validator' import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { setCookie } from 'hono/cookie' import { setCookie } from 'hono/cookie'
import { TimeSpan } from 'oslo' import { TimeSpan } from 'oslo'
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe'
import { limiter } from '../middleware/rate-limiter.middleware' import { limiter } from '../middleware/rate-limiter.middleware'
import { LoginRequestsService } from '../services/loginrequest.service' import { LoginRequestsService } from '../services/loginrequest.service'
import type { HonoTypes } from '../types'
@injectable() @injectable()
export class LoginController implements Controller { export class LoginController extends Controller {
controller = new Hono<HonoTypes>()
constructor( constructor(
@inject(LoginRequestsService) private readonly loginRequestsService: LoginRequestsService, @inject(LoginRequestsService) private readonly loginRequestsService: LoginRequestsService,
@inject(LuciaProvider) private lucia: LuciaProvider, @inject(LuciaService) private luciaService: LuciaService,
) {} ) {
super()
}
routes() { routes() {
return this.controller.post('/', zValidator('json', signinUsernameDto), limiter({ limit: 10, minutes: 60 }), async (c) => { return this.controller.post('/', zValidator('json', signinUsernameDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { username, password } = c.req.valid('json') const { username, password } = c.req.valid('json')
const session = await this.loginRequestsService.verify({ username, password }, c.req) const session = await this.loginRequestsService.verify({ username, password }, c.req)
const sessionCookie = this.lucia.createSessionCookie(session.id) const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id)
console.log('set cookie', sessionCookie) console.log('set cookie', sessionCookie)
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,

View file

@ -1,27 +1,24 @@
import 'reflect-metadata' import 'reflect-metadata'
import { StatusCodes } from '$lib/constants/status-codes' import { StatusCodes } from '$lib/constants/status-codes'
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface' import { Controller } from '$lib/server/api/common/types/controller'
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto' import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto'
import { db } from '$lib/server/api/packages/drizzle'
import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service' import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service'
import { TotpService } from '$lib/server/api/services/totp.service' import { TotpService } from '$lib/server/api/services/totp.service'
import { UsersService } from '$lib/server/api/services/users.service' import { UsersService } from '$lib/server/api/services/users.service'
import { zValidator } from '@hono/zod-validator' import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe'
import { CredentialsType } from '../databases/tables' import { CredentialsType } from '../databases/tables'
import { requireAuth } from '../middleware/auth.middleware' import { requireAuth } from '../middleware/require-auth.middleware'
import type { HonoTypes } from '../types'
@injectable() @injectable()
export class MfaController implements Controller { export class MfaController extends Controller {
controller = new Hono<HonoTypes>()
constructor( constructor(
@inject(RecoveryCodesService) private readonly recoveryCodesService: RecoveryCodesService, @inject(RecoveryCodesService) private readonly recoveryCodesService: RecoveryCodesService,
@inject(TotpService) private readonly totpService: TotpService, @inject(TotpService) private readonly totpService: TotpService,
@inject(UsersService) private readonly usersService: UsersService, @inject(UsersService) private readonly usersService: UsersService,
) {} ) {
super()
}
routes() { routes() {
return this.controller return this.controller
@ -52,8 +49,9 @@ export class MfaController implements Controller {
const user = c.var.user const user = c.var.user
// You can only view recovery codes once and that is on creation // You can only view recovery codes once and that is on creation
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id) const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id)
if (existingCodes) { if (existingCodes && existingCodes.length > 0) {
return c.body('You have already generated recovery codes', StatusCodes.BAD_REQUEST) console.log('Recovery Codes found', existingCodes)
return c.json({ recoveryCodes: existingCodes })
} }
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id) const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id)
return c.json({ recoveryCodes }) return c.json({ recoveryCodes })

View file

@ -0,0 +1,150 @@
import 'reflect-metadata'
import { Controller } from '$lib/server/api/common/types/controller'
import { LuciaService } from '$lib/server/api/services/lucia.service'
import { OAuthService } from '$lib/server/api/services/oauth.service'
import { github, google } from '$lib/server/auth'
import { OAuth2RequestError } from 'arctic'
import { getCookie, setCookie } from 'hono/cookie'
import { TimeSpan } from 'oslo'
import { inject, injectable } from 'tsyringe'
import type {OAuthUser} from "$lib/server/api/common/types/oauth";
@injectable()
export class OAuthController extends Controller {
constructor(
@inject(LuciaService) private luciaService: LuciaService,
@inject(OAuthService) private oauthService: OAuthService,
) {
super()
}
routes() {
return this.controller
.get('/github', async (c) => {
try {
const code = c.req.query('code')?.toString() ?? null
const state = c.req.query('state')?.toString() ?? null
const storedState = getCookie(c).github_oauth_state ?? null
if (!code || !state || !storedState || state !== storedState) {
return c.body(null, 400)
}
const tokens = await github.validateAuthorizationCode(code)
const githubUserResponse = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
const githubUser: GitHubUser = await githubUserResponse.json()
const oAuthUser: OAuthUser = {
sub: `${githubUser.id}`,
username: githubUser.login,
email: undefined
}
const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'github')
const session = await this.luciaService.lucia.createSession(userId, {})
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id)
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge:
sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
? sessionCookie.attributes.maxAge
: new TimeSpan(2, 'w').seconds(),
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
})
return c.json({ message: 'ok' })
} catch (error) {
console.error(error)
// the specific error message depends on the provider
if (error instanceof OAuth2RequestError) {
// invalid code
return c.body(null, 400)
}
return c.body(null, 500)
}
})
.get('/google', async (c) => {
try {
const code = c.req.query('code')?.toString() ?? null
const state = c.req.query('state')?.toString() ?? null
const storedState = getCookie(c).google_oauth_state ?? null
const storedCodeVerifier = getCookie(c).google_oauth_code_verifier ?? null
if (!code || !storedState || !storedCodeVerifier || state !== storedState) {
return c.body(null, 400)
}
const tokens = await google.validateAuthorizationCode(code, storedCodeVerifier)
const googleUserResponse = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
const googleUser: GoogleUser = await googleUserResponse.json()
const oAuthUser: OAuthUser = {
sub: googleUser.sub,
given_name: googleUser.given_name,
family_name: googleUser.family_name,
picture: googleUser.picture,
username: googleUser.email,
email: googleUser.email,
email_verified: googleUser.email_verified,
}
const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'google')
const session = await this.luciaService.lucia.createSession(userId, {})
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id)
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge:
sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
? sessionCookie.attributes.maxAge
: new TimeSpan(2, 'w').seconds(),
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
})
return c.json({ message: 'ok' })
} catch (error) {
console.error(error)
// the specific error message depends on the provider
if (error instanceof OAuth2RequestError) {
// invalid code
return c.body(null, 400)
}
return c.body(null, 500)
}
})
}
}
interface GitHubUser {
id: number
login: string
}
interface GoogleUser {
sub: string
name: string
given_name: string
family_name: string
picture: string
email: string
email_verified: boolean
}

View file

@ -1,26 +1,24 @@
import 'reflect-metadata' import 'reflect-metadata'
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface' import { Controller } from '$lib/server/api/common/types/controller'
import { signupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto' import { signupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto'
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware' import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import { LuciaProvider } from '$lib/server/api/providers/lucia.provider'
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service' import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service'
import { LuciaService } from '$lib/server/api/services/lucia.service'
import { UsersService } from '$lib/server/api/services/users.service' import { UsersService } from '$lib/server/api/services/users.service'
import { zValidator } from '@hono/zod-validator' import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { setCookie } from 'hono/cookie' import { setCookie } from 'hono/cookie'
import { TimeSpan } from 'oslo' import { TimeSpan } from 'oslo'
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe'
import type { HonoTypes } from '../types'
@injectable() @injectable()
export class SignupController implements Controller { export class SignupController extends Controller {
controller = new Hono<HonoTypes>()
constructor( constructor(
@inject(UsersService) private readonly usersService: UsersService, @inject(UsersService) private readonly usersService: UsersService,
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService, @inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
@inject(LuciaProvider) private lucia: LuciaProvider, @inject(LuciaService) private luciaService: LuciaService,
) {} ) {
super()
}
routes() { routes() {
return this.controller.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { return this.controller.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
@ -38,7 +36,7 @@ export class SignupController implements Controller {
} }
const session = await this.loginRequestService.createUserSession(user.id, c.req, undefined) const session = await this.loginRequestService.createUserSession(user.id, c.req, undefined)
const sessionCookie = this.lucia.createSessionCookie(session.id) const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id)
console.log('set cookie', sessionCookie) console.log('set cookie', sessionCookie)
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,

View file

@ -1,16 +1,14 @@
import 'reflect-metadata' import 'reflect-metadata'
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface' import { Controller } from '$lib/server/api/common/types/controller'
import { UsersService } from '$lib/server/api/services/users.service' import { UsersService } from '$lib/server/api/services/users.service'
import { Hono } from 'hono'
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware' import { requireAuth } from '../middleware/require-auth.middleware'
import type { HonoTypes } from '../types'
@injectable() @injectable()
export class UserController implements Controller { export class UserController extends Controller {
controller = new Hono<HonoTypes>() constructor(@inject(UsersService) private readonly usersService: UsersService) {
super()
constructor(@inject(UsersService) private readonly usersService: UsersService) {} }
routes() { routes() {
return this.controller return this.controller

View file

@ -1,16 +1,14 @@
import 'reflect-metadata' import 'reflect-metadata'
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface' import { Controller } from '$lib/server/api/common/types/controller'
import { WishlistsService } from '$lib/server/api/services/wishlists.service' import { WishlistsService } from '$lib/server/api/services/wishlists.service'
import { Hono } from 'hono'
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware' import { requireAuth } from '../middleware/require-auth.middleware'
import type { HonoTypes } from '../types'
@injectable() @injectable()
export class WishlistController implements Controller { export class WishlistController extends Controller {
controller = new Hono<HonoTypes>() constructor(@inject(WishlistsService) private readonly wishlistsService: WishlistsService) {
super()
constructor(@inject(WishlistsService) private readonly wishlistsService: WishlistsService) {} }
routes() { routes() {
return this.controller return this.controller

View file

@ -3,7 +3,7 @@ import { drizzle } from 'drizzle-orm/postgres-js'
import { migrate } from 'drizzle-orm/postgres-js/migrator' import { migrate } from 'drizzle-orm/postgres-js/migrator'
import postgres from 'postgres' import postgres from 'postgres'
import config from '../../../../../drizzle.config' import config from '../../../../../drizzle.config'
import env from '../../../../env' import env from '../common/env'
const connection = postgres({ const connection = postgres({
host: env.DATABASE_HOST || 'localhost', host: env.DATABASE_HOST || 'localhost',
@ -17,7 +17,11 @@ const connection = postgres({
const db = drizzle(connection) const db = drizzle(connection)
try { try {
await migrate(db, { migrationsFolder: config.out! }) if (!config.out) {
console.error('No migrations folder specified in drizzle.config.ts')
process.exit()
}
await migrate(db, { migrationsFolder: config.out })
console.log('Migrations complete') console.log('Migrations complete')
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View file

@ -47,6 +47,15 @@ CREATE TABLE IF NOT EXISTS "collections" (
CONSTRAINT "collections_cuid_unique" UNIQUE("cuid") CONSTRAINT "collections_cuid_unique" UNIQUE("cuid")
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE IF NOT EXISTS "credentials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"type" text DEFAULT 'password' NOT NULL,
"secret_data" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "expansions" ( CREATE TABLE IF NOT EXISTS "expansions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text, "cuid" text,
@ -65,6 +74,16 @@ CREATE TABLE IF NOT EXISTS "external_ids" (
CONSTRAINT "external_ids_cuid_unique" UNIQUE("cuid") CONSTRAINT "external_ids_cuid_unique" UNIQUE("cuid")
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE IF NOT EXISTS "federated_identity" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"identity_provider" text NOT NULL,
"federated_user_id" text NOT NULL,
"federated_username" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "games" ( CREATE TABLE IF NOT EXISTS "games" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text, "cuid" text,
@ -177,9 +196,9 @@ CREATE TABLE IF NOT EXISTS "sessions" (
CREATE TABLE IF NOT EXISTS "two_factor" ( CREATE TABLE IF NOT EXISTS "two_factor" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text, "cuid" text,
"two_factor_secret" text NOT NULL, "secret" text NOT NULL,
"two_factor_enabled" boolean DEFAULT false NOT NULL, "enabled" boolean DEFAULT false NOT NULL,
"initiated_time" timestamp with time zone NOT NULL, "initiated_time" timestamp with time zone,
"user_id" uuid NOT NULL, "user_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
@ -202,12 +221,12 @@ CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text, "cuid" text,
"username" text, "username" text,
"hashed_password" text,
"email" text, "email" text,
"first_name" text, "first_name" text,
"last_name" text, "last_name" text,
"verified" boolean DEFAULT false, "verified" boolean DEFAULT false,
"receive_email" boolean DEFAULT false, "receive_email" boolean DEFAULT false,
"mfa_enabled" boolean DEFAULT false NOT NULL,
"theme" text DEFAULT 'system', "theme" text DEFAULT 'system',
"created_at" timestamp with time zone DEFAULT now() NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
@ -278,6 +297,12 @@ EXCEPTION
WHEN duplicate_object THEN null; WHEN duplicate_object THEN null;
END $$; END $$;
--> statement-breakpoint --> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "credentials" ADD CONSTRAINT "credentials_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN DO $$ BEGIN
ALTER TABLE "expansions" ADD CONSTRAINT "expansions_base_game_id_games_id_fk" FOREIGN KEY ("base_game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade; ALTER TABLE "expansions" ADD CONSTRAINT "expansions_base_game_id_games_id_fk" FOREIGN KEY ("base_game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION EXCEPTION
@ -290,6 +315,12 @@ EXCEPTION
WHEN duplicate_object THEN null; WHEN duplicate_object THEN null;
END $$; END $$;
--> statement-breakpoint --> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "federated_identity" ADD CONSTRAINT "federated_identity_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN DO $$ BEGIN
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade; ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION EXCEPTION

View file

@ -1,2 +0,0 @@
ALTER TABLE "two_factor" RENAME COLUMN "two_factor_secret" TO "secret";--> statement-breakpoint
ALTER TABLE "two_factor" RENAME COLUMN "two_factor_enabled" TO "enabled";

View file

@ -0,0 +1,2 @@
ALTER TABLE "users" ADD COLUMN "email_verified" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "picture" text;

View file

@ -1 +0,0 @@
ALTER TABLE "two_factor" ALTER COLUMN "initiated_time" DROP NOT NULL;

View file

@ -1,32 +0,0 @@
CREATE TABLE IF NOT EXISTS "credentials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"type" text DEFAULT 'password' NOT NULL,
"secret_data" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "federated_identity" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"identity_provider" text NOT NULL,
"federated_user_id" text NOT NULL,
"federated_username" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "credentials" ADD CONSTRAINT "credentials_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "federated_identity" ADD CONSTRAINT "federated_identity_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN IF EXISTS "hashed_password";

View file

@ -1 +0,0 @@
ALTER TABLE "users" ADD COLUMN "enabled" boolean DEFAULT false NOT NULL;

View file

@ -1 +0,0 @@
ALTER TABLE "users" RENAME COLUMN "enabled" TO "mfa_enabled";

View file

@ -1,5 +1,5 @@
{ {
"id": "e120d11a-bf28-4c96-9f2f-96e23e23c7e2", "id": "4760134e-48bb-47db-b431-56903dad6e24",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
@ -338,6 +338,70 @@
} }
} }
}, },
"public.credentials": {
"name": "credentials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'password'"
},
"secret_data": {
"name": "secret_data",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"credentials_user_id_users_id_fk": {
"name": "credentials_user_id_users_id_fk",
"tableFrom": "credentials",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.expansions": { "public.expansions": {
"name": "expansions", "name": "expansions",
"schema": "", "schema": "",
@ -466,6 +530,75 @@
} }
} }
}, },
"public.federated_identity": {
"name": "federated_identity",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"identity_provider": {
"name": "identity_provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"federated_user_id": {
"name": "federated_user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"federated_username": {
"name": "federated_username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"federated_identity_user_id_users_id_fk": {
"name": "federated_identity_user_id_users_id_fk",
"tableFrom": "federated_identity",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.games": { "public.games": {
"name": "games", "name": "games",
"schema": "", "schema": "",
@ -1273,14 +1406,14 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"two_factor_secret": { "secret": {
"name": "two_factor_secret", "name": "secret",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}, },
"two_factor_enabled": { "enabled": {
"name": "two_factor_enabled", "name": "enabled",
"type": "boolean", "type": "boolean",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
@ -1290,7 +1423,7 @@
"name": "initiated_time", "name": "initiated_time",
"type": "timestamp with time zone", "type": "timestamp with time zone",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": false
}, },
"user_id": { "user_id": {
"name": "user_id", "name": "user_id",
@ -1461,12 +1594,6 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"hashed_password": {
"name": "hashed_password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": { "email": {
"name": "email", "name": "email",
"type": "text", "type": "text",
@ -1499,6 +1626,13 @@
"notNull": false, "notNull": false,
"default": false "default": false
}, },
"mfa_enabled": {
"name": "mfa_enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"theme": { "theme": {
"name": "theme", "name": "theme",
"type": "text", "type": "text",
@ -1720,6 +1854,7 @@
} }
}, },
"schemas": {}, "schemas": {},
"sequences": {},
"_meta": { "_meta": {
"columns": {}, "columns": {},
"schemas": {}, "schemas": {},

View file

@ -1,6 +1,6 @@
{ {
"id": "52e7c416-89cb-4c6a-9118-68a03cfc2920", "id": "e1230cae-67ce-4669-885a-3d3fe4462f9e",
"prevId": "e120d11a-bf28-4c96-9f2f-96e23e23c7e2", "prevId": "4760134e-48bb-47db-b431-56903dad6e24",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
"tables": { "tables": {
@ -338,6 +338,70 @@
} }
} }
}, },
"public.credentials": {
"name": "credentials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'password'"
},
"secret_data": {
"name": "secret_data",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"credentials_user_id_users_id_fk": {
"name": "credentials_user_id_users_id_fk",
"tableFrom": "credentials",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.expansions": { "public.expansions": {
"name": "expansions", "name": "expansions",
"schema": "", "schema": "",
@ -466,6 +530,75 @@
} }
} }
}, },
"public.federated_identity": {
"name": "federated_identity",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"identity_provider": {
"name": "identity_provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"federated_user_id": {
"name": "federated_user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"federated_username": {
"name": "federated_username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"federated_identity_user_id_users_id_fk": {
"name": "federated_identity_user_id_users_id_fk",
"tableFrom": "federated_identity",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.games": { "public.games": {
"name": "games", "name": "games",
"schema": "", "schema": "",
@ -1290,7 +1423,7 @@
"name": "initiated_time", "name": "initiated_time",
"type": "timestamp with time zone", "type": "timestamp with time zone",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": false
}, },
"user_id": { "user_id": {
"name": "user_id", "name": "user_id",
@ -1461,12 +1594,6 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"hashed_password": {
"name": "hashed_password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": { "email": {
"name": "email", "name": "email",
"type": "text", "type": "text",
@ -1499,6 +1626,26 @@
"notNull": false, "notNull": false,
"default": false "default": false
}, },
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"picture": {
"name": "picture",
"type": "text",
"primaryKey": false,
"notNull": false
},
"mfa_enabled": {
"name": "mfa_enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"theme": { "theme": {
"name": "theme", "name": "theme",
"type": "text", "type": "text",
@ -1720,6 +1867,7 @@
} }
}, },
"schemas": {}, "schemas": {},
"sequences": {},
"_meta": { "_meta": {
"columns": {}, "columns": {},
"schemas": {}, "schemas": {},

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -5,43 +5,15 @@
{ {
"idx": 0, "idx": 0,
"version": "7", "version": "7",
"when": 1720625651245, "when": 1725489682980,
"tag": "0000_dazzling_stick", "tag": "0000_volatile_warhawk",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "7", "version": "7",
"when": 1720625948784, "when": 1726877846811,
"tag": "0001_noisy_sally_floyd", "tag": "0001_pink_the_enforcers",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1720626020902,
"tag": "0002_fancy_valkyrie",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1723593488634,
"tag": "0003_worried_taskmaster",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1725055403926,
"tag": "0004_heavy_sphinx",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1725055643756,
"tag": "0005_true_mathemanic",
"breakpoints": true "breakpoints": true
} }
] ]

View file

@ -1,14 +1,18 @@
import { Table, getTableName, sql } from 'drizzle-orm' import 'reflect-metadata'
import env from '../../../../env' import { DrizzleService } from '$lib/server/api/services/drizzle.service'
import { db, pool } from '../packages/drizzle' import { type Table, getTableName, sql } from 'drizzle-orm'
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import env from '../common/env'
import * as seeds from './seeds' import * as seeds from './seeds'
import * as schema from './tables' import * as schema from './tables'
const drizzleService = new DrizzleService()
if (!env.DB_SEEDING) { if (!env.DB_SEEDING) {
throw new Error('You must set DB_SEEDING to "true" when running seeds') throw new Error('You must set DB_SEEDING to "true" when running seeds')
} }
async function resetTable(db: db, table: Table) { async function resetTable(db: NodePgDatabase<typeof schema>, table: Table) {
return db.execute(sql.raw(`TRUNCATE TABLE ${getTableName(table)} RESTART IDENTITY CASCADE`)) return db.execute(sql.raw(`TRUNCATE TABLE ${getTableName(table)} RESTART IDENTITY CASCADE`))
} }
@ -19,33 +23,33 @@ for (const table of [
schema.collection_items, schema.collection_items,
schema.collections, schema.collections,
schema.credentialsTable, schema.credentialsTable,
schema.expansions, schema.expansionsTable,
schema.externalIds, schema.externalIdsTable,
schema.federatedIdentityTable, schema.federatedIdentityTable,
schema.games, schema.gamesTable,
schema.gamesToExternalIds, schema.gamesToExternalIdsTable,
schema.mechanics, schema.mechanicsTable,
schema.mechanicsToExternalIds, schema.mechanicsToExternalIdsTable,
schema.mechanics_to_games, schema.mechanics_to_games,
schema.password_reset_tokens, schema.password_reset_tokens,
schema.publishers, schema.publishersTable,
schema.publishersToExternalIds, schema.publishersToExternalIdsTable,
schema.publishers_to_games, schema.publishers_to_games,
schema.recoveryCodesTable, schema.recoveryCodesTable,
schema.roles, schema.rolesTable,
schema.sessionsTable, schema.sessionsTable,
schema.twoFactorTable, schema.twoFactorTable,
schema.user_roles, schema.user_roles,
schema.usersTable, schema.usersTable,
schema.wishlist_items, schema.wishlist_items,
schema.wishlists, schema.wishlistsTable,
]) { ]) {
// await db.delete(table); // clear tables without truncating / resetting ids // await db.delete(table); // clear tables without truncating / resetting ids
await resetTable(db, table) await resetTable(drizzleService.db, table)
} }
await seeds.roles(db) await seeds.roles(drizzleService.db)
await seeds.users(db) await seeds.users(drizzleService.db)
await pool.end() await drizzleService.dispose()
process.exit() process.exit()

View file

@ -1,11 +1,11 @@
import * as schema from '$lib/server/api/databases/tables' import type { db } from '../../packages/drizzle'
import { type db } from '$lib/server/api/packages/drizzle' import * as schema from '../tables'
import roles from './data/roles.json' import roles from './data/roles.json'
export default async function seed(db: db) { export default async function seed(db: db) {
console.log('Creating roles ...') console.log('Creating rolesTable ...')
for (const role of roles) { for (const role of roles) {
await db.insert(schema.roles).values(role).onConflictDoNothing() await db.insert(schema.rolesTable).values(role).onConflictDoNothing()
} }
console.log('Roles created.') console.log('Roles created.')
} }

View file

@ -1,35 +1,24 @@
import * as schema from '$lib/server/api/databases/tables'
import { type db } from '$lib/server/api/packages/drizzle'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { Argon2id } from 'oslo/password' import type { db } from '../../packages/drizzle'
import { config } from '../../configs/config' import { HashingService } from '../../services/hashing.service'
import * as schema from '../tables'
import users from './data/users.json' import users from './data/users.json'
type JsonUser = {
id: string
username: string
email: string
password: string
roles: {
name: string
primary: boolean
}[]
}
type JsonRole = { type JsonRole = {
name: string name: string
primary: boolean primary: boolean
} }
export default async function seed(db: db) { export default async function seed(db: db) {
const adminRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'admin')) const hashingService = new HashingService()
const userRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'user')) const adminRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'admin'))
const userRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'user'))
console.log('Admin Role: ', adminRole) console.log('Admin Role: ', adminRole)
const adminUser = await db const adminUser = await db
.insert(schema.usersTable) .insert(schema.usersTable)
.values({ .values({
username: `${config.ADMIN_USERNAME}`, username: `${process.env.ADMIN_USERNAME}`,
email: '', email: '',
first_name: 'Brad', first_name: 'Brad',
last_name: 'S', last_name: 'S',
@ -43,12 +32,12 @@ export default async function seed(db: db) {
await db.insert(schema.credentialsTable).values({ await db.insert(schema.credentialsTable).values({
user_id: adminUser[0].id, user_id: adminUser[0].id,
type: schema.CredentialsType.PASSWORD, type: schema.CredentialsType.PASSWORD,
secret_data: await new Argon2id().hash(`${config.ADMIN_PASSWORD}`), secret_data: await hashingService.hash(`${process.env.ADMIN_PASSWORD}`),
}) })
await db.insert(schema.collections).values({ user_id: adminUser[0].id }).onConflictDoNothing() await db.insert(schema.collections).values({ user_id: adminUser[0].id }).onConflictDoNothing()
await db.insert(schema.wishlists).values({ user_id: adminUser[0].id }).onConflictDoNothing() await db.insert(schema.wishlistsTable).values({ user_id: adminUser[0].id }).onConflictDoNothing()
await db await db
.insert(schema.user_roles) .insert(schema.user_roles)
@ -71,6 +60,7 @@ export default async function seed(db: db) {
.onConflictDoNothing() .onConflictDoNothing()
console.log('Admin user given user role.') console.log('Admin user given user role.')
const hasingService = new HashingService()
await Promise.all( await Promise.all(
users.map(async (user) => { users.map(async (user) => {
const [insertedUser] = await db const [insertedUser] = await db
@ -82,14 +72,14 @@ export default async function seed(db: db) {
await db.insert(schema.credentialsTable).values({ await db.insert(schema.credentialsTable).values({
user_id: insertedUser?.id, user_id: insertedUser?.id,
type: schema.CredentialsType.PASSWORD, type: schema.CredentialsType.PASSWORD,
secret_data: await new Argon2id().hash(user.password), secret_data: await hasingService.hash(user.password),
}) })
await db.insert(schema.collections).values({ user_id: insertedUser?.id }) await db.insert(schema.collections).values({ user_id: insertedUser?.id })
await db.insert(schema.wishlists).values({ user_id: insertedUser?.id }) await db.insert(schema.wishlistsTable).values({ user_id: insertedUser?.id })
await Promise.all( await Promise.all(
user.roles.map(async (role: JsonRole) => { user.roles.map(async (role: JsonRole) => {
const foundRole = await db.query.roles.findFirst({ const foundRole = await db.query.rolesTable.findFirst({
where: eq(schema.roles.name, role.name), where: eq(schema.rolesTable.name, role.name),
}) })
if (!foundRole) { if (!foundRole) {
throw new Error('Role not found') throw new Error('Role not found')

View file

@ -1,9 +1,9 @@
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table.utils' import { timestamps } from '../../common/utils/table'
import { categoriesToExternalIdsTable } from './categoriesToExternalIdsTable' import { categoriesToExternalIdsTable } from './categoriesToExternalIds.table'
import { categories_to_games_table } from './categoriesToGames' import { categories_to_games_table } from './categoriesToGames.table'
export const categoriesTable = pgTable('categories', { export const categoriesTable = pgTable('categories', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),

View file

@ -0,0 +1,34 @@
import { relations } from 'drizzle-orm'
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { categoriesTable } from './categories.table'
import { externalIdsTable } from './externalIds.table'
export const categoriesToExternalIdsTable = pgTable(
'categories_to_external_ids',
{
categoryId: uuid('category_id')
.notNull()
.references(() => categoriesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
.notNull()
.references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
categoriesToExternalIdsPkey: primaryKey({
columns: [table.categoryId, table.externalId],
}),
}
},
)
export const categoriesToExternalIdsRelations = relations(categoriesToExternalIdsTable, ({ one }) => ({
category: one(categoriesTable, {
fields: [categoriesToExternalIdsTable.categoryId],
references: [categoriesTable.id],
}),
externalId: one(externalIdsTable, {
fields: [categoriesToExternalIdsTable.externalId],
references: [externalIdsTable.id],
}),
}))

View file

@ -1,37 +0,0 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { categoriesTable } from './categories.table';
import { externalIds } from './externalIds';
import { relations } from 'drizzle-orm';
export const categoriesToExternalIdsTable = pgTable(
'categories_to_external_ids',
{
categoryId: uuid('category_id')
.notNull()
.references(() => categoriesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
.notNull()
.references(() => externalIds.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
categoriesToExternalIdsPkey: primaryKey({
columns: [table.categoryId, table.externalId],
}),
};
},
);
export const categoriesToExternalIdsRelations = relations(
categoriesToExternalIdsTable,
({ one }) => ({
category: one(categoriesTable, {
fields: [categoriesToExternalIdsTable.categoryId],
references: [categoriesTable.id],
}),
externalId: one(externalIds, {
fields: [categoriesToExternalIdsTable.externalId],
references: [externalIds.id],
}),
}),
);

View file

@ -1,7 +1,7 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'
import { relations } from 'drizzle-orm'; import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { categoriesTable } from './categories.table'; import { gamesTable } from '././games.table'
import { games } from './games'; import { categoriesTable } from './categories.table'
export const categories_to_games_table = pgTable( export const categories_to_games_table = pgTable(
'categories_to_games', 'categories_to_games',
@ -11,25 +11,24 @@ export const categories_to_games_table = pgTable(
.references(() => categoriesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => categoriesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id') game_id: uuid('game_id')
.notNull() .notNull()
.references(() => games.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
}, },
(table) => { (table) => {
return { return {
categoriesToGamesPkey: primaryKey({ categoriesToGamesPkey: primaryKey({
columns: [table.category_id, table.game_id], columns: [table.category_id, table.game_id],
}), }),
}; }
}, },
); )
export const categories_to_games_relations = relations(categories_to_games_table, ({ one }) => ({ export const categories_to_games_relations = relations(categories_to_games_table, ({ one }) => ({
category: one(categoriesTable, { category: one(categoriesTable, {
fields: [categories_to_games_table.category_id], fields: [categories_to_games_table.category_id],
references: [categoriesTable.id], references: [categoriesTable.id],
}), }),
game: one(games, { game: one(gamesTable, {
fields: [categories_to_games_table.game_id], fields: [categories_to_games_table.game_id],
references: [games.id], references: [gamesTable.id],
}), }),
})); }))

View file

@ -1,9 +1,9 @@
import { timestamps } from '$lib/server/api/common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { integer, pgTable, text, uuid } from 'drizzle-orm/pg-core' import { integer, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { collections } from './collections' import { timestamps } from '../../common/utils/table'
import { games } from './games' import { gamesTable } from '././games.table'
import { collections } from './collections.table'
export const collection_items = pgTable('collection_items', { export const collection_items = pgTable('collection_items', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@ -15,20 +15,20 @@ export const collection_items = pgTable('collection_items', {
.references(() => collections.id, { onDelete: 'cascade' }), .references(() => collections.id, { onDelete: 'cascade' }),
game_id: uuid('game_id') game_id: uuid('game_id')
.notNull() .notNull()
.references(() => games.id, { onDelete: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'cascade' }),
times_played: integer('times_played').default(0), times_played: integer('times_played').default(0),
...timestamps, ...timestamps,
}) })
export type CollectionItems = InferSelectModel<typeof collection_items> export type CollectionItemsTable = InferSelectModel<typeof collection_items>
export const collection_item_relations = relations(collection_items, ({ one }) => ({ export const collection_item_relations = relations(collection_items, ({ one }) => ({
collection: one(collections, { collection: one(collections, {
fields: [collection_items.collection_id], fields: [collection_items.collection_id],
references: [collections.id], references: [collections.id],
}), }),
game: one(games, { game: one(gamesTable, {
fields: [collection_items.game_id], fields: [collection_items.game_id],
references: [games.id], references: [gamesTable.id],
}), }),
})) }))

View file

@ -1,8 +1,9 @@
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table.utils' import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table' import { usersTable } from './users.table'
import { collection_items } from './collectionItems.table'
export const collections = pgTable('collections', { export const collections = pgTable('collections', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@ -16,11 +17,12 @@ export const collections = pgTable('collections', {
...timestamps, ...timestamps,
}) })
export const collection_relations = relations(collections, ({ one }) => ({ export const collection_relations = relations(collections, ({ one, many }) => ({
user: one(usersTable, { user: one(usersTable, {
fields: [collections.user_id], fields: [collections.user_id],
references: [usersTable.id], references: [usersTable.id],
}), }),
collection_items: many(collection_items),
})) }))
export type Collections = InferSelectModel<typeof collections> export type Collections = InferSelectModel<typeof collections>

View file

@ -1,6 +1,6 @@
import { type InferSelectModel } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table.utils' import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table' import { usersTable } from './users.table'
export enum CredentialsType { export enum CredentialsType {

View file

@ -0,0 +1,32 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { gamesTable } from '././games.table'
export const expansionsTable = pgTable('expansions', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
base_game_id: uuid('base_game_id')
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id')
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
...timestamps,
})
export type Expansions = InferSelectModel<typeof expansionsTable>
export const expansion_relations = relations(expansionsTable, ({ one }) => ({
baseGame: one(gamesTable, {
fields: [expansionsTable.base_game_id],
references: [gamesTable.id],
}),
game: one(gamesTable, {
fields: [expansionsTable.game_id],
references: [gamesTable.id],
}),
}))

View file

@ -0,0 +1,16 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import type { InferSelectModel } from 'drizzle-orm'
import { pgEnum, pgTable, text, uuid } from 'drizzle-orm/pg-core'
export const externalIdType = pgEnum('external_id_type', ['game', 'category', 'mechanic', 'publisher', 'designer', 'artist'])
export const externalIdsTable = pgTable('external_ids', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
type: externalIdType('type'),
externalId: text('external_id').notNull(),
})
export type ExternalIds = InferSelectModel<typeof externalIdsTable>

View file

@ -1,23 +0,0 @@
import { pgEnum, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import type { InferSelectModel } from 'drizzle-orm';
export const externalIdType = pgEnum('external_id_type', [
'game',
'category',
'mechanic',
'publisher',
'designer',
'artist',
]);
export const externalIds = pgTable('external_ids', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
type: externalIdType('type'),
externalId: text('external_id').notNull(),
});
export type ExternalIds = InferSelectModel<typeof externalIds>;

View file

@ -1,6 +1,6 @@
import { type InferSelectModel } from 'drizzle-orm' import { type InferSelectModel } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table.utils' import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table' import { usersTable } from './users.table'
export const federatedIdentityTable = pgTable('federated_identity', { export const federatedIdentityTable = pgTable('federated_identity', {

View file

@ -1,13 +1,13 @@
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations, sql } from 'drizzle-orm' import { type InferSelectModel, relations, sql } from 'drizzle-orm'
import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table.utils' import { timestamps } from '../../common/utils/table'
import { categories_to_games_table } from './categoriesToGames' import { categories_to_games_table } from './categoriesToGames.table'
import { gamesToExternalIds } from './gamesToExternalIds' import { gamesToExternalIdsTable } from './gamesToExternalIds.table'
import { mechanics_to_games } from './mechanicsToGames' import { mechanics_to_games } from './mechanicsToGames.table'
import { publishers_to_games } from './publishersToGames' import { publishers_to_games } from './publishersToGames.table'
export const games = pgTable( export const gamesTable = pgTable(
'games', 'games',
{ {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@ -41,11 +41,11 @@ export const games = pgTable(
}), }),
) )
export const gameRelations = relations(games, ({ many }) => ({ export const gameRelations = relations(gamesTable, ({ many }) => ({
categories_to_games: many(categories_to_games_table), categories_to_games: many(categories_to_games_table),
mechanics_to_games: many(mechanics_to_games), mechanics_to_games: many(mechanics_to_games),
publishers_to_games: many(publishers_to_games), publishers_to_games: many(publishers_to_games),
gamesToExternalIds: many(gamesToExternalIds), gamesToExternalIds: many(gamesToExternalIdsTable),
})) }))
export type Games = InferSelectModel<typeof games> export type Games = InferSelectModel<typeof gamesTable>

View file

@ -0,0 +1,34 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { gamesTable } from '././games.table'
import { externalIdsTable } from './externalIds.table'
import { relations } from 'drizzle-orm'
export const gamesToExternalIdsTable = pgTable(
'games_to_external_ids',
{
gameId: uuid('game_id')
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
.notNull()
.references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
gamesToExternalIdsPkey: primaryKey({
columns: [table.gameId, table.externalId],
}),
}
},
)
export const gamesToExternalIdsRelations = relations(gamesToExternalIdsTable, ({ one }) => ({
game: one(gamesTable, {
fields: [gamesToExternalIdsTable.gameId],
references: [gamesTable.id],
}),
externalId: one(externalIdsTable, {
fields: [gamesToExternalIdsTable.externalId],
references: [externalIdsTable.id],
}),
}))

View file

@ -1,22 +0,0 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import {games} from './games';
import {externalIds} from './externalIds';
export const gamesToExternalIds = pgTable(
'games_to_external_ids',
{
gameId: uuid('game_id')
.notNull()
.references(() => games.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
.notNull()
.references(() => externalIds.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
gamesToExternalIdsPkey: primaryKey({
columns: [table.gameId, table.externalId],
}),
};
},
);

View file

@ -1,26 +1,26 @@
export * from './categories.table'; export * from './categories.table'
export * from './categoriesToExternalIdsTable'; export * from './categoriesToExternalIds.table'
export * from './categoriesToGames'; export * from './categoriesToGames.table'
export * from './collectionItems'; export * from './collectionItems.table'
export * from './collections'; export * from './collections.table'
export * from './credentials.table'; export * from './credentials.table'
export * from './expansions'; export * from './expansions.table'
export * from './externalIds'; export * from './externalIds.table'
export * from './federatedIdentity.table'; export * from './federatedIdentity.table'
export * from './games'; export * from './games.table'
export * from './gamesToExternalIds'; export * from './gamesToExternalIds.table'
export * from './mechanics'; export * from './mechanics.table'
export * from './mechanicsToExternalIds'; export * from './mechanicsToExternalIds.table'
export * from './mechanicsToGames' export * from './mechanicsToGames.table'
export * from './passwordResetTokens'; export * from './passwordResetTokens.table'
export * from './publishers'; export * from './publishers.table'
export * from './publishersToExternalIds'; export * from './publishersToExternalIds.table'
export * from './publishersToGames'; export * from './publishersToGames.table'
export * from './recovery-codes.table'; export * from './recovery-codes.table'
export * from './roles'; export * from './roles.table'
export * from './sessions.table'; export * from './sessions.table'
export * from './two-factor.table'; export * from './two-factor.table'
export * from './userRoles'; export * from './userRoles.table'
export * from './users.table'; export * from './users.table'
export * from './wishlistItems'; export * from './wishlistItems.table'
export * from './wishlists'; export * from './wishlists.table'

View file

@ -0,0 +1,23 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { mechanicsToExternalIdsTable } from './mechanicsToExternalIds.table'
import { mechanics_to_games } from './mechanicsToGames.table'
export const mechanicsTable = pgTable('mechanics', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
name: text('name'),
slug: text('slug'),
...timestamps,
})
export type Mechanics = InferSelectModel<typeof mechanicsTable>
export const mechanics_relations = relations(mechanicsTable, ({ many }) => ({
mechanics_to_games: many(mechanics_to_games),
mechanicsToExternalIds: many(mechanicsToExternalIdsTable),
}))

View file

@ -0,0 +1,34 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { externalIdsTable } from './externalIds.table'
import { mechanicsTable } from './mechanics.table'
import { relations } from 'drizzle-orm'
export const mechanicsToExternalIdsTable = pgTable(
'mechanics_to_external_ids',
{
mechanicId: uuid('mechanic_id')
.notNull()
.references(() => mechanicsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
.notNull()
.references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
mechanicsToExternalIdsPkey: primaryKey({
columns: [table.mechanicId, table.externalId],
}),
}
},
)
export const mechanicsToExternalIdsRelations = relations(mechanicsToExternalIdsTable, ({ one }) => ({
mechanic: one(mechanicsTable, {
fields: [mechanicsToExternalIdsTable.mechanicId],
references: [mechanicsTable.id],
}),
externalId: one(externalIdsTable, {
fields: [mechanicsToExternalIdsTable.externalId],
references: [externalIdsTable.id],
}),
}))

View file

@ -1,22 +0,0 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import {mechanics} from './mechanics';
import {externalIds} from './externalIds';
export const mechanicsToExternalIds = pgTable(
'mechanics_to_external_ids',
{
mechanicId: uuid('mechanic_id')
.notNull()
.references(() => mechanics.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
.notNull()
.references(() => externalIds.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
mechanicsToExternalIdsPkey: primaryKey({
columns: [table.mechanicId, table.externalId],
}),
};
},
);

View file

@ -0,0 +1,34 @@
import { relations } from 'drizzle-orm'
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { gamesTable } from '././games.table'
import { mechanicsTable } from './mechanics.table'
export const mechanics_to_games = pgTable(
'mechanics_to_games',
{
mechanic_id: uuid('mechanic_id')
.notNull()
.references(() => mechanicsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id')
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
mechanicsToGamesPkey: primaryKey({
columns: [table.mechanic_id, table.game_id],
}),
}
},
)
export const mechanics_to_games_relations = relations(mechanics_to_games, ({ one }) => ({
mechanic: one(mechanicsTable, {
fields: [mechanics_to_games.mechanic_id],
references: [mechanicsTable.id],
}),
game: one(gamesTable, {
fields: [mechanics_to_games.game_id],
references: [gamesTable.id],
}),
}))

View file

@ -1,34 +0,0 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import {mechanics} from './mechanics';
import {games} from './games';
export const mechanics_to_games = pgTable(
'mechanics_to_games',
{
mechanic_id: uuid('mechanic_id')
.notNull()
.references(() => mechanics.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id')
.notNull()
.references(() => games.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
mechanicsToGamesPkey: primaryKey({
columns: [table.mechanic_id, table.game_id],
}),
};
},
);
export const mechanics_to_games_relations = relations(mechanics_to_games, ({ one }) => ({
mechanic: one(mechanics, {
fields: [mechanics_to_games.mechanic_id],
references: [mechanics.id],
}),
game: one(games, {
fields: [mechanics_to_games.game_id],
references: [games.id],
}),
}));

View file

@ -1,7 +1,7 @@
import { timestamps } from '$lib/server/api/common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table' import { usersTable } from './users.table'
export const password_reset_tokens = pgTable('password_reset_tokens', { export const password_reset_tokens = pgTable('password_reset_tokens', {
@ -15,7 +15,7 @@ export const password_reset_tokens = pgTable('password_reset_tokens', {
...timestamps, ...timestamps,
}) })
export type PasswordResetTokens = InferSelectModel<typeof password_reset_tokens> export type PasswordResetTokensTable = InferSelectModel<typeof password_reset_tokens>
export const password_reset_token_relations = relations(password_reset_tokens, ({ one }) => ({ export const password_reset_token_relations = relations(password_reset_tokens, ({ one }) => ({
user: one(usersTable, { user: one(usersTable, {

View file

@ -0,0 +1,23 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { publishersToExternalIdsTable } from './publishersToExternalIds.table'
import { publishers_to_games } from './publishersToGames.table'
export const publishersTable = pgTable('publishers', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
name: text('name'),
slug: text('slug'),
...timestamps,
})
export type Publishers = InferSelectModel<typeof publishersTable>
export const publishers_relations = relations(publishersTable, ({ many }) => ({
publishersToGames: many(publishers_to_games),
publishersToExternalIds: many(publishersToExternalIdsTable),
}))

View file

@ -0,0 +1,34 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { externalIdsTable } from './externalIds.table'
import { publishersTable } from './publishers.table'
import { relations } from 'drizzle-orm'
export const publishersToExternalIdsTable = pgTable(
'publishers_to_external_ids',
{
publisherId: uuid('publisher_id')
.notNull()
.references(() => publishersTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
.notNull()
.references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
publishersToExternalIdsPkey: primaryKey({
columns: [table.publisherId, table.externalId],
}),
}
},
)
export const publishersToExternalIdsRelations = relations(publishersToExternalIdsTable, ({ one }) => ({
publisher: one(publishersTable, {
fields: [publishersToExternalIdsTable.publisherId],
references: [publishersTable.id],
}),
externalId: one(externalIdsTable, {
fields: [publishersToExternalIdsTable.externalId],
references: [externalIdsTable.id],
}),
}))

View file

@ -1,22 +0,0 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import {publishers} from './publishers';
import {externalIds} from './externalIds';
export const publishersToExternalIds = pgTable(
'publishers_to_external_ids',
{
publisherId: uuid('publisher_id')
.notNull()
.references(() => publishers.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
.notNull()
.references(() => externalIds.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
(table) => {
return {
publishersToExternalIdsPkey: primaryKey({
columns: [table.publisherId, table.externalId],
}),
};
},
);

View file

@ -1,34 +1,34 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'
import { relations } from 'drizzle-orm'; import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import {publishers} from './publishers'; import { gamesTable } from '././games.table'
import {games} from './games'; import { publishersTable } from './publishers.table'
export const publishers_to_games = pgTable( export const publishers_to_games = pgTable(
'publishers_to_games', 'publishers_to_games',
{ {
publisher_id: uuid('publisher_id') publisher_id: uuid('publisher_id')
.notNull() .notNull()
.references(() => publishers.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => publishersTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id') game_id: uuid('game_id')
.notNull() .notNull()
.references(() => games.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
}, },
(table) => { (table) => {
return { return {
publishersToGamesPkey: primaryKey({ publishersToGamesPkey: primaryKey({
columns: [table.publisher_id, table.game_id], columns: [table.publisher_id, table.game_id],
}), }),
}; }
}, },
); )
export const publishers_to_games_relations = relations(publishers_to_games, ({ one }) => ({ export const publishers_to_games_relations = relations(publishers_to_games, ({ one }) => ({
publisher: one(publishers, { publisher: one(publishersTable, {
fields: [publishers_to_games.publisher_id], fields: [publishers_to_games.publisher_id],
references: [publishers.id], references: [publishersTable.id],
}), }),
game: one(games, { game: one(gamesTable, {
fields: [publishers_to_games.game_id], fields: [publishers_to_games.game_id],
references: [games.id], references: [gamesTable.id],
}), }),
})); }))

View file

@ -1,6 +1,6 @@
import { timestamps } from '../../common/utils/table.utils'
import type { InferSelectModel } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm'
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core' import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table' import { usersTable } from './users.table'
export const recoveryCodesTable = pgTable('recovery_codes', { export const recoveryCodesTable = pgTable('recovery_codes', {

View file

@ -1,10 +1,17 @@
import { timestamps } from '../../common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { user_roles } from './userRoles' import { timestamps } from '../../common/utils/table'
import { user_roles } from './userRoles.table'
export const roles = pgTable('roles', { export enum RoleName {
ADMIN = 'admin',
EDITOR = 'editor',
MODERATOR = 'moderator',
USER = 'user',
}
export const rolesTable = pgTable('roles', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid') cuid: text('cuid')
.unique() .unique()
@ -14,8 +21,8 @@ export const roles = pgTable('roles', {
...timestamps, ...timestamps,
}) })
export type Roles = InferSelectModel<typeof roles> export type Roles = InferSelectModel<typeof rolesTable>
export const role_relations = relations(roles, ({ many }) => ({ export const role_relations = relations(rolesTable, ({ many }) => ({
user_roles: many(user_roles), user_roles: many(user_roles),
})) }))

View file

@ -1,7 +1,7 @@
import { timestamps } from '../../common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table' import { usersTable } from './users.table'
export const twoFactorTable = pgTable('two_factor', { export const twoFactorTable = pgTable('two_factor', {

View file

@ -1,8 +1,8 @@
import { timestamps } from '../../common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core' import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { roles } from './roles' import { timestamps } from '../../common/utils/table'
import { rolesTable } from './roles.table'
import { usersTable } from './users.table' import { usersTable } from './users.table'
export const user_roles = pgTable('user_roles', { export const user_roles = pgTable('user_roles', {
@ -15,15 +15,15 @@ export const user_roles = pgTable('user_roles', {
.references(() => usersTable.id, { onDelete: 'cascade' }), .references(() => usersTable.id, { onDelete: 'cascade' }),
role_id: uuid('role_id') role_id: uuid('role_id')
.notNull() .notNull()
.references(() => roles.id, { onDelete: 'cascade' }), .references(() => rolesTable.id, { onDelete: 'cascade' }),
primary: boolean('primary').default(false), primary: boolean('primary').default(false),
...timestamps, ...timestamps,
}) })
export const user_role_relations = relations(user_roles, ({ one }) => ({ export const user_role_relations = relations(user_roles, ({ one }) => ({
role: one(roles, { role: one(rolesTable, {
fields: [user_roles.role_id], fields: [user_roles.role_id],
references: [roles.id], references: [rolesTable.id],
}), }),
user: one(usersTable, { user: one(usersTable, {
fields: [user_roles.user_id], fields: [user_roles.user_id],
@ -31,4 +31,4 @@ export const user_role_relations = relations(user_roles, ({ one }) => ({
}), }),
})) }))
export type UserRoles = InferSelectModel<typeof user_roles> export type UserRolesTable = InferSelectModel<typeof user_roles>

View file

@ -1,8 +1,8 @@
import { timestamps } from '../../common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core' import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { user_roles } from './userRoles' import { timestamps } from '../../common/utils/table'
import { user_roles } from './userRoles.table'
export const usersTable = pgTable('users', { export const usersTable = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@ -15,6 +15,8 @@ export const usersTable = pgTable('users', {
last_name: text('last_name'), last_name: text('last_name'),
verified: boolean('verified').default(false), verified: boolean('verified').default(false),
receive_email: boolean('receive_email').default(false), receive_email: boolean('receive_email').default(false),
email_verified: boolean('email_verified').default(false),
picture: text('picture'),
mfa_enabled: boolean('mfa_enabled').notNull().default(false), mfa_enabled: boolean('mfa_enabled').notNull().default(false),
theme: text('theme').default('system'), theme: text('theme').default('system'),
...timestamps, ...timestamps,

View file

@ -1,9 +1,9 @@
import { timestamps } from '../../common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { games } from './games' import { timestamps } from '../../common/utils/table'
import { wishlists } from './wishlists' import { gamesTable } from '././games.table'
import { wishlistsTable } from './wishlists.table'
export const wishlist_items = pgTable('wishlist_items', { export const wishlist_items = pgTable('wishlist_items', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@ -12,22 +12,22 @@ export const wishlist_items = pgTable('wishlist_items', {
.$defaultFn(() => cuid2()), .$defaultFn(() => cuid2()),
wishlist_id: uuid('wishlist_id') wishlist_id: uuid('wishlist_id')
.notNull() .notNull()
.references(() => wishlists.id, { onDelete: 'cascade' }), .references(() => wishlistsTable.id, { onDelete: 'cascade' }),
game_id: uuid('game_id') game_id: uuid('game_id')
.notNull() .notNull()
.references(() => games.id, { onDelete: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'cascade' }),
...timestamps, ...timestamps,
}) })
export type WishlistItems = InferSelectModel<typeof wishlist_items> export type WishlistItemsTable = InferSelectModel<typeof wishlist_items>
export const wishlist_item_relations = relations(wishlist_items, ({ one }) => ({ export const wishlist_item_relations = relations(wishlist_items, ({ one }) => ({
wishlist: one(wishlists, { wishlist: one(wishlistsTable, {
fields: [wishlist_items.wishlist_id], fields: [wishlist_items.wishlist_id],
references: [wishlists.id], references: [wishlistsTable.id],
}), }),
game: one(games, { game: one(gamesTable, {
fields: [wishlist_items.game_id], fields: [wishlist_items.game_id],
references: [games.id], references: [gamesTable.id],
}), }),
})) }))

View file

@ -1,10 +1,10 @@
import { timestamps } from '../../common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table' import { usersTable } from './users.table'
export const wishlists = pgTable('wishlists', { export const wishlistsTable = pgTable('wishlists', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid') cuid: text('cuid')
.unique() .unique()
@ -16,11 +16,11 @@ export const wishlists = pgTable('wishlists', {
...timestamps, ...timestamps,
}) })
export type Wishlists = InferSelectModel<typeof wishlists> export type Wishlists = InferSelectModel<typeof wishlistsTable>
export const wishlists_relations = relations(wishlists, ({ one }) => ({ export const wishlists_relations = relations(wishlistsTable, ({ one }) => ({
user: one(usersTable, { user: one(usersTable, {
fields: [wishlists.user_id], fields: [wishlistsTable.user_id],
references: [usersTable.id], references: [usersTable.id],
}), }),
})) }))

View file

@ -0,0 +1,17 @@
import { refinePasswords } from '$lib/validations/account'
import { z } from 'zod'
export const changePasswordDto = z
.object({
password: z.string({ required_error: 'Password is required' }).trim(),
confirm_password: z
.string({ required_error: 'Confirm Password is required' })
.trim()
.min(8, { message: 'Must be at least 8 characters' })
.max(128, { message: 'Must be less than 128 characters' }),
})
.superRefine(({ confirm_password, password }, ctx) => {
return refinePasswords(confirm_password, password, ctx)
})
export type ChangePasswordDto = z.infer<typeof changePasswordDto>

View file

@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from 'zod'
export const updateProfileDto = z.object({ export const updateProfileDto = z.object({
firstName: z firstName: z
@ -7,17 +7,8 @@ export const updateProfileDto = z.object({
.min(3, { message: 'Must be at least 3 characters' }) .min(3, { message: 'Must be at least 3 characters' })
.max(50, { message: 'Must be less than 50 characters' }) .max(50, { message: 'Must be less than 50 characters' })
.optional(), .optional(),
lastName: z lastName: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }).optional(),
.string() username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
.trim() })
.min(3, {message: 'Must be at least 3 characters'})
.max(50, {message: 'Must be less than 50 characters'})
.optional(),
username: z
.string()
.trim()
.min(3, {message: 'Must be at least 3 characters'})
.max(50, {message: 'Must be less than 50 characters'})
});
export type UpdateProfileDto = z.infer<typeof updateProfileDto>; export type UpdateProfileDto = z.infer<typeof updateProfileDto>

View file

@ -1,6 +1,7 @@
import 'reflect-metadata' import 'reflect-metadata'
import { CollectionController } from '$lib/server/api/controllers/collection.controller' import { CollectionController } from '$lib/server/api/controllers/collection.controller'
import { MfaController } from '$lib/server/api/controllers/mfa.controller' import { MfaController } from '$lib/server/api/controllers/mfa.controller'
import { OAuthController } from '$lib/server/api/controllers/oauth.controller'
import { SignupController } from '$lib/server/api/controllers/signup.controller' import { SignupController } from '$lib/server/api/controllers/signup.controller'
import { UserController } from '$lib/server/api/controllers/user.controller' import { UserController } from '$lib/server/api/controllers/user.controller'
import { WishlistController } from '$lib/server/api/controllers/wishlist.controller' import { WishlistController } from '$lib/server/api/controllers/wishlist.controller'
@ -10,7 +11,7 @@ import { hc } from 'hono/client'
import { cors } from 'hono/cors' import { cors } from 'hono/cors'
import { logger } from 'hono/logger' import { logger } from 'hono/logger'
import { container } from 'tsyringe' import { container } from 'tsyringe'
import { config } from './configs/config' import { config } from './common/config'
import { IamController } from './controllers/iam.controller' import { IamController } from './controllers/iam.controller'
import { LoginController } from './controllers/login.controller' import { LoginController } from './controllers/login.controller'
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware' import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware'
@ -44,6 +45,7 @@ const routes = app
.route('/me', container.resolve(IamController).routes()) .route('/me', container.resolve(IamController).routes())
.route('/user', container.resolve(UserController).routes()) .route('/user', container.resolve(UserController).routes())
.route('/login', container.resolve(LoginController).routes()) .route('/login', container.resolve(LoginController).routes())
.route('/oauth', container.resolve(OAuthController).routes())
.route('/signup', container.resolve(SignupController).routes()) .route('/signup', container.resolve(SignupController).routes())
.route('/wishlists', container.resolve(WishlistController).routes()) .route('/wishlists', container.resolve(WishlistController).routes())
.route('/collections', container.resolve(CollectionController).routes()) .route('/collections', container.resolve(CollectionController).routes())

View file

@ -10,11 +10,11 @@ export class AuthCleanupJobs {
this.queue = this.jobsService.createQueue('test') this.queue = this.jobsService.createQueue('test')
/* ---------------------------- Register Workers ---------------------------- */ /* ---------------------------- Register Workers ---------------------------- */
this.worker().then((r) => console.log('auth-cleanup job worker started')) this.worker().then(() => console.log('auth-cleanup job worker started'))
} }
async deleteStaleEmailVerificationRequests() { async deleteStaleEmailVerificationRequests() {
await this.queue.add('delete_stale_email_verifiactions', null, { await this.queue.add('delete_stale_email_verifications', null, {
repeat: { repeat: {
pattern: '0 0 * * 0', // Runs once a week at midnight on Sunday pattern: '0 0 * * 0', // Runs once a week at midnight on Sunday
}, },
@ -31,7 +31,7 @@ export class AuthCleanupJobs {
private async worker() { private async worker() {
return this.jobsService.createWorker(this.queue.name, async (job) => { return this.jobsService.createWorker(this.queue.name, async (job) => {
if (job.name === 'delete_stale_email_verifiactions') { if (job.name === 'delete_stale_email_verifications') {
// delete stale email verifications // delete stale email verifications
} }
if (job.name === 'delete_stale_login_requests') { if (job.name === 'delete_stale_login_requests') {

View file

@ -1,10 +1,12 @@
import { LuciaService } from '$lib/server/api/services/lucia.service'
import type { MiddlewareHandler } from 'hono' import type { MiddlewareHandler } from 'hono'
import { createMiddleware } from 'hono/factory' import { createMiddleware } from 'hono/factory'
import type { Session, User } from 'lucia'
import { verifyRequestOrigin } from 'oslo/request' import { verifyRequestOrigin } from 'oslo/request'
import { Unauthorized } from '../common/exceptions' import { container } from 'tsyringe'
import { lucia } from '../packages/lucia' import type { HonoTypes } from '../common/types/hono'
import type { HonoTypes } from '../types'
// resolve dependencies from the container
const { lucia } = container.resolve(LuciaService)
export const verifyOrigin: MiddlewareHandler<HonoTypes> = createMiddleware(async (c, next) => { export const verifyOrigin: MiddlewareHandler<HonoTypes> = createMiddleware(async (c, next) => {
if (c.req.method === 'GET') { if (c.req.method === 'GET') {
@ -27,7 +29,7 @@ export const validateAuthSession: MiddlewareHandler<HonoTypes> = createMiddlewar
} }
const { session, user } = await lucia.validateSession(sessionId) const { session, user } = await lucia.validateSession(sessionId)
if (session && session.fresh) { if (session?.fresh) {
c.header('Set-Cookie', lucia.createSessionCookie(session.id).serialize(), { append: true }) c.header('Set-Cookie', lucia.createSessionCookie(session.id).serialize(), { append: true })
} }
if (!session) { if (!session) {
@ -37,14 +39,3 @@ export const validateAuthSession: MiddlewareHandler<HonoTypes> = createMiddlewar
c.set('user', user) c.set('user', user)
return next() return next()
}) })
export const requireAuth: MiddlewareHandler<{
Variables: {
session: Session
user: User
}
}> = createMiddleware(async (c, next) => {
const user = c.var.user
if (!user) throw Unauthorized('You must be logged in to access this resource')
return next()
})

View file

@ -1,10 +1,11 @@
import { rateLimiter } from 'hono-rate-limiter' import { rateLimiter } from 'hono-rate-limiter'
import RedisClient from 'ioredis'
import { RedisStore } from 'rate-limit-redis' import { RedisStore } from 'rate-limit-redis'
import { config } from '../configs/config' import { container } from 'tsyringe'
import type { HonoTypes } from '../types' import type { HonoTypes } from '../common/types/hono'
import { RedisService } from '../services/redis.service'
const client = new RedisClient(config.REDIS_URL) // resolve dependencies from the container
const { client } = container.resolve(RedisService)
export function limiter({ export function limiter({
limit, limit,

View file

@ -0,0 +1,15 @@
import { Unauthorized } from '$lib/server/api/common/exceptions'
import type { MiddlewareHandler } from 'hono'
import { createMiddleware } from 'hono/factory'
import type { Session, User } from 'lucia'
export const requireAuth: MiddlewareHandler<{
Variables: {
session: Session
user: User
}
}> = createMiddleware(async (c, next) => {
const user = c.var.user
if (!user) throw Unauthorized('You must be logged in to access this resource')
return next()
})

View file

@ -1,10 +0,0 @@
import { Argon2id } from "oslo/password";
export async function hash(value: string) {
const argon2 = new Argon2id()
return argon2.hash(value);
}
export function verify(hashedValue: string, value: string) {
return new Argon2id().verify(hashedValue, value);
}

View file

@ -1,6 +1,6 @@
import { drizzle } from 'drizzle-orm/node-postgres' import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg' import pg from 'pg'
import { config } from '../configs/config' import { config } from '../common/config'
import * as schema from '../databases/tables' import * as schema from '../databases/tables'
// create the connection // create the connection

View file

@ -1,63 +0,0 @@
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle'
// lib/server/lucia.ts
import { Lucia, TimeSpan } from 'lucia'
import { config } from '../configs/config'
import { sessionsTable, usersTable } from '../databases/tables'
import { db } from './drizzle'
const adapter = new DrizzlePostgreSQLAdapter(db, sessionsTable, usersTable)
export const lucia = new Lucia(adapter, {
getSessionAttributes: (attributes) => {
return {
ipCountry: attributes.ip_country,
ipAddress: attributes.ip_address,
isTwoFactorAuthEnabled: attributes.twoFactorAuthEnabled,
isTwoFactorAuthenticated: attributes.isTwoFactorAuthenticated,
}
},
getUserAttributes: (attributes) => {
return {
// ...attributes,
username: attributes.username,
email: attributes.email,
firstName: attributes.first_name,
lastName: attributes.last_name,
mfa_enabled: attributes.mfa_enabled,
theme: attributes.theme,
}
},
sessionExpiresIn: new TimeSpan(2, 'w'), // 2 weeks
sessionCookie: {
name: 'session',
expires: false, // session cookies have very long lifespan (2 years)
attributes: {
// set to `true` when using HTTPS
secure: config.isProduction,
sameSite: 'strict',
domain: config.domain,
},
},
})
declare module 'lucia' {
interface Register {
Lucia: typeof lucia
DatabaseUserAttributes: DatabaseUserAttributes
DatabaseSessionAttributes: DatabaseSessionAttributes
}
interface DatabaseSessionAttributes {
ip_country: string
ip_address: string
twoFactorAuthEnabled: boolean
isTwoFactorAuthenticated: boolean
}
interface DatabaseUserAttributes {
username: string
email: string
first_name: string
last_name: string
mfa_enabled: boolean
theme: string
}
}

Some files were not shown because too many files have changed in this diff Show more