Copying over tables, migrations, seeds, etc. Creating DTOs for future use.

This commit is contained in:
Bradley Shellnut 2024-07-24 17:39:03 -07:00
parent d70b3061b5
commit 16191509b4
56 changed files with 6900 additions and 13 deletions

View file

@ -4,8 +4,8 @@ import env from './src/env';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema/index.ts',
out: './src/db/migrations',
out: './src/lib/server/api/infrastructure/database/migrations',
schema: './src/lib/server/api/infrastructure/database/tables/index.ts',
dbCredentials: {
host: env.DATABASE_HOST || 'localhost',
port: Number(env.DATABASE_PORT) || 5432,
@ -18,4 +18,8 @@ export default defineConfig({
verbose: true,
// Always as for confirmation
strict: true,
migrations: {
table: 'migrations',
schema: 'public'
}
});

View file

@ -31,9 +31,9 @@ for (const table of [
schema.publishers_to_games,
schema.recoveryCodes,
schema.roles,
schema.sessions,
schema.sessionsTable,
schema.userRoles,
schema.users,
schema.usersTable,
schema.twoFactor,
schema.wishlists,
schema.wishlist_items,

View file

@ -1,4 +1,3 @@
import { eq } from 'drizzle-orm';
import { type db } from '$db';
import * as schema from '$db/schema';
import roles from './data/roles.json';

View file

@ -27,7 +27,7 @@ export default async function seed(db: db) {
console.log('Admin Role: ', adminRole);
const adminUser = await db
.insert(schema.users)
.insert(schema.usersTable)
.values({
username: `${env.ADMIN_USERNAME}`,
email: '',
@ -73,7 +73,7 @@ export default async function seed(db: db) {
await Promise.all(
users.map(async (user) => {
const [insertedUser] = await db
.insert(schema.users)
.insert(schema.usersTable)
.values({
...user,
hashed_password: await new Argon2id().hash(user.password),

View file

@ -0,0 +1,20 @@
import { z } from "zod";
import { refinePasswords } from "$lib/validations/account";
export const registerEmailPasswordDto = z.object({
firstName: z.string().trim().optional(),
lastName: z.string().trim().optional(),
email: z.string().trim().max(64, { message: 'Email must be less than 64 characters' }).optional(),
username: z
.string()
.trim()
.min(3, { message: 'Must be at least 3 characters' })
.max(50, { message: 'Must be less than 50 characters' }),
password: z.string({ required_error: 'Password is required' }).trim(),
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
})
.superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx);
});
export type RegisterEmailPasswordDto = z.infer<typeof registerEmailPasswordDto>;

View file

@ -0,0 +1,12 @@
import { z } from "zod";
export const signInEmailDto = z.object({
username: z
.string()
.trim()
.min(3, { message: 'Must be at least 3 characters' })
.max(50, { message: 'Must be less than 50 characters' }),
password: z.string({ required_error: 'Password is required' }).trim(),
});
export type SignInEmailDto = z.infer<typeof signInEmailDto>;

View file

@ -1,10 +1,23 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { requireAuth } from "../middleware/auth.middleware";
import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto';
import { limiter } from '../middleware/rate-limiter.middleware';
const users = new Hono().get('/me', requireAuth, async (c) => {
const users = new Hono()
.get('/me', requireAuth, async (c) => {
const user = c.var.user;
return c.json({ user });
});
})
.get('/user', requireAuth, async (c) => {
const user = c.var.user;
return c.json({ user });
})
.post('/login/request', zValidator('json', registerEmailPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { email } = c.req.valid('json');
await this.loginRequestsService.create({ email });
return c.json({ message: 'Verification email sent' });
});
export default users;
export type UsersType = typeof users

View file

@ -13,11 +13,14 @@ app.use(verifyOrigin).use(validateAuthSession);
/* --------------------------------- Routes --------------------------------- */
const routes = app
.route('/iam', users)
.get('/', (c) => c.json({ message: 'Server is healthy' }));
/* -------------------------------------------------------------------------- */
/* Exports */
/* -------------------------------------------------------------------------- */
export const rpc = hc<typeof routes>(config.ORIGIN);
export type AppType = typeof routes;
export const rpc = hc<AppType>(config.ORIGIN);
export type ApiClient = typeof rpc;
export type ApiRoutes = typeof routes;
export { app };

View file

@ -0,0 +1,22 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import { config } from '../../common/config';
import * as schema from './tables';
// create the connection
export const pool = new pg.Pool({
user: config.DATABASE_USER,
password: config.DATABASE_PASSWORD,
host: config.DATABASE_HOST,
port: Number(config.DATABASE_PORT).valueOf(),
database: config.DATABASE_DB,
ssl: config.DATABASE_HOST !== 'localhost',
max: config.DB_MIGRATING || config.DB_SEEDING ? 1 : undefined,
});
export const db = drizzle(pool, {
schema,
logger: config.NODE_ENV === 'development',
});
export type db = typeof db;

View file

@ -0,0 +1,26 @@
import 'dotenv/config';
import postgres from 'postgres';
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import env from '../env';
import config from '../../drizzle.config';
const connection = postgres({
host: env.DATABASE_HOST || 'localhost',
port: env.DATABASE_PORT,
user: env.DATABASE_USER || 'root',
password: env.DATABASE_PASSWORD || '',
database: env.DATABASE_DB || 'boredgame',
ssl: env.NODE_ENV === 'development' ? false : 'require',
max: 1,
});
const db = drizzle(connection);
try {
await migrate(db, { migrationsFolder: config.out! });
console.log('Migrations complete');
} catch (e) {
console.error(e);
}
process.exit();

View file

@ -0,0 +1,410 @@
DO $$ BEGIN
CREATE TYPE "public"."external_id_type" AS ENUM('game', 'category', 'mechanic', 'publisher', 'designer', 'artist');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "categories" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"name" text,
"slug" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "categories_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "categories_to_external_ids" (
"category_id" uuid NOT NULL,
"external_id" uuid NOT NULL,
CONSTRAINT "categories_to_external_ids_category_id_external_id_pk" PRIMARY KEY("category_id","external_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "categories_to_games" (
"category_id" uuid NOT NULL,
"game_id" uuid NOT NULL,
CONSTRAINT "categories_to_games_category_id_game_id_pk" PRIMARY KEY("category_id","game_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "collection_items" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"collection_id" uuid NOT NULL,
"game_id" uuid NOT NULL,
"times_played" integer DEFAULT 0,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "collection_items_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "collections" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"user_id" uuid NOT NULL,
"name" text DEFAULT 'My Collection' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "collections_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "expansions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"base_game_id" uuid NOT NULL,
"game_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "expansions_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "external_ids" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"type" "external_id_type",
"external_id" text NOT NULL,
CONSTRAINT "external_ids_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "games" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"name" text NOT NULL,
"slug" text NOT NULL,
"description" text,
"year_published" integer,
"min_players" integer,
"max_players" integer,
"playtime" integer,
"min_playtime" integer,
"max_playtime" integer,
"min_age" integer,
"image_url" text,
"thumb_url" text,
"url" text,
"last_sync_at" timestamp,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "games_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "games_to_external_ids" (
"game_id" uuid NOT NULL,
"external_id" uuid NOT NULL,
CONSTRAINT "games_to_external_ids_game_id_external_id_pk" PRIMARY KEY("game_id","external_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "mechanics" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"name" text,
"slug" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "mechanics_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "mechanics_to_external_ids" (
"mechanic_id" uuid NOT NULL,
"external_id" uuid NOT NULL,
CONSTRAINT "mechanics_to_external_ids_mechanic_id_external_id_pk" PRIMARY KEY("mechanic_id","external_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "mechanics_to_games" (
"mechanic_id" uuid NOT NULL,
"game_id" uuid NOT NULL,
CONSTRAINT "mechanics_to_games_mechanic_id_game_id_pk" PRIMARY KEY("mechanic_id","game_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "password_reset_tokens" (
"id" text PRIMARY KEY NOT NULL,
"user_id" uuid NOT NULL,
"expires_at" timestamp,
"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 "publishers" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"name" text,
"slug" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "publishers_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "publishers_to_external_ids" (
"publisher_id" uuid NOT NULL,
"external_id" uuid NOT NULL,
CONSTRAINT "publishers_to_external_ids_publisher_id_external_id_pk" PRIMARY KEY("publisher_id","external_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "publishers_to_games" (
"publisher_id" uuid NOT NULL,
"game_id" uuid NOT NULL,
CONSTRAINT "publishers_to_games_publisher_id_game_id_pk" PRIMARY KEY("publisher_id","game_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "recovery_codes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"code" text NOT NULL,
"used" boolean DEFAULT false,
"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 "roles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text NOT NULL,
"name" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "roles_cuid_unique" UNIQUE("cuid"),
CONSTRAINT "roles_name_unique" UNIQUE("name")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "sessions" (
"id" text PRIMARY KEY NOT NULL,
"user_id" uuid NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"ip_country" text,
"ip_address" text,
"two_factor_auth_enabled" boolean DEFAULT false,
"is_two_factor_authenticated" boolean DEFAULT false
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "two_factor" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"two_factor_secret" text NOT NULL,
"two_factor_enabled" boolean DEFAULT false NOT NULL,
"initiated_time" timestamp with time zone NOT NULL,
"user_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "two_factor_cuid_unique" UNIQUE("cuid"),
CONSTRAINT "two_factor_user_id_unique" UNIQUE("user_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "user_roles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"user_id" uuid NOT NULL,
"role_id" uuid NOT NULL,
"primary" boolean DEFAULT false,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "user_roles_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"username" text,
"hashed_password" text,
"email" text,
"first_name" text,
"last_name" text,
"verified" boolean DEFAULT false,
"receive_email" boolean DEFAULT false,
"theme" text DEFAULT 'system',
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "users_cuid_unique" UNIQUE("cuid"),
CONSTRAINT "users_username_unique" UNIQUE("username"),
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "wishlist_items" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"wishlist_id" uuid NOT NULL,
"game_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "wishlist_items_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "wishlists" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"user_id" uuid NOT NULL,
"name" text DEFAULT 'My Wishlist' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "wishlists_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "public"."external_ids"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_games" ADD CONSTRAINT "categories_to_games_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_games" ADD CONSTRAINT "categories_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "collection_items" ADD CONSTRAINT "collection_items_collection_id_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."collections"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "collection_items" ADD CONSTRAINT "collection_items_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "collections" ADD CONSTRAINT "collections_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 "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
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expansions" ADD CONSTRAINT "expansions_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
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;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "public"."external_ids"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_mechanic_id_mechanics_id_fk" FOREIGN KEY ("mechanic_id") REFERENCES "public"."mechanics"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "public"."external_ids"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_games" ADD CONSTRAINT "mechanics_to_games_mechanic_id_mechanics_id_fk" FOREIGN KEY ("mechanic_id") REFERENCES "public"."mechanics"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_games" ADD CONSTRAINT "mechanics_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "password_reset_tokens" ADD CONSTRAINT "password_reset_tokens_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 "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_publisher_id_publishers_id_fk" FOREIGN KEY ("publisher_id") REFERENCES "public"."publishers"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "public"."external_ids"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_games" ADD CONSTRAINT "publishers_to_games_publisher_id_publishers_id_fk" FOREIGN KEY ("publisher_id") REFERENCES "public"."publishers"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_games" ADD CONSTRAINT "publishers_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "recovery_codes" ADD CONSTRAINT "recovery_codes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_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 "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_wishlist_id_wishlists_id_fk" FOREIGN KEY ("wishlist_id") REFERENCES "public"."wishlists"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "wishlists" ADD CONSTRAINT "wishlists_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
CREATE INDEX IF NOT EXISTS "search_index" ON "games" USING gin ((
setweight(to_tsvector('english', "name"), 'A') ||
setweight(to_tsvector('english', "slug"), 'B')
));

View file

@ -0,0 +1,2 @@
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 @@
ALTER TABLE "two_factor" ALTER COLUMN "initiated_time" DROP NOT NULL;

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

@ -0,0 +1,27 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1720625651245,
"tag": "0000_dazzling_stick",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1720625948784,
"tag": "0001_noisy_sally_floyd",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1720626020902,
"tag": "0002_fancy_valkyrie",
"breakpoints": true
}
]
}

View file

@ -0,0 +1,49 @@
import { Table, getTableName, sql } from 'drizzle-orm';
import env from '../env';
import { db, pool } from '$db';
import * as schema from './tables';
import * as seeds from './seeds';
if (!env.DB_SEEDING) {
throw new Error('You must set DB_SEEDING to "true" when running seeds');
}
async function resetTable(db: db, table: Table) {
return db.execute(sql.raw(`TRUNCATE TABLE ${getTableName(table)} RESTART IDENTITY CASCADE`));
}
for (const table of [
schema.categories,
schema.categoriesToExternalIds,
schema.categories_to_games,
schema.collection_items,
schema.collections,
schema.expansions,
schema.externalIds,
schema.games,
schema.gamesToExternalIds,
schema.mechanics,
schema.mechanicsToExternalIds,
schema.mechanics_to_games,
schema.password_reset_tokens,
schema.publishers,
schema.publishersToExternalIds,
schema.publishers_to_games,
schema.recoveryCodes,
schema.roles,
schema.sessionsTable,
schema.userRoles,
schema.usersTable,
schema.twoFactor,
schema.wishlists,
schema.wishlist_items,
]) {
// await db.delete(table); // clear tables without truncating / resetting ids
await resetTable(db, table);
}
await seeds.roles(db);
await seeds.users(db);
await pool.end();
process.exit();

View file

@ -0,0 +1,14 @@
[
{
"name": "admin"
},
{
"name": "user"
},
{
"name": "editor"
},
{
"name": "moderator"
}
]

View file

@ -0,0 +1,62 @@
[
{
"first_name": "John",
"last_name": "Smith",
"username": "john.smith",
"email": "john.smith@example.com",
"password": "password",
"roles": [
{
"name": "user",
"primary": true
}
]
},
{
"first_name": "Jane",
"last_name": "Doe",
"username": "jane.doe",
"email": "jane.doe@example.com",
"password": "password",
"roles": [
{
"name": "user",
"primary": true
}
]
},
{
"first_name": "Michael",
"last_name": "Editor",
"username": "michael.editor",
"email": "michael.editor@example.com",
"password": "password",
"roles": [
{
"name": "editor",
"primary": true
},
{
"name": "user",
"primary": false
}
]
},
{
"first_name": "Jane",
"last_name": "Moderator",
"username": "jane.moderator",
"email": "jane.moderator@example.com",
"password": "password",
"roles": [
{
"name": "moderator",
"primary": true
},
{
"name": "user",
"primary": false
}
]
}
]

View file

@ -0,0 +1,2 @@
export { default as users } from './users';
export { default as roles } from './roles';

View file

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

View file

@ -0,0 +1,98 @@
import { eq } from 'drizzle-orm';
import { Argon2id } from 'oslo/password';
import { type db } from '$db';
import * as schema from '$db/schema';
import users from './data/users.json';
import { config } from '../../../common/config';
type JsonUser = {
id: string;
username: string;
email: string;
password: string;
roles: {
name: string;
primary: boolean;
}[];
};
type JsonRole = {
name: string;
primary: boolean;
};
export default async function seed(db: db) {
const adminRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'admin'));
const userRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'user'));
console.log('Admin Role: ', adminRole);
const adminUser = await db
.insert(schema.usersTable)
.values({
username: `${config.ADMIN_USERNAME}`,
email: '',
hashed_password: await new Argon2id().hash(`${config.ADMIN_PASSWORD}`),
first_name: 'Brad',
last_name: 'S',
verified: true,
})
.returning()
.onConflictDoNothing();
console.log('Admin user created.', adminUser);
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.userRoles)
.values({
user_id: adminUser[0].id,
role_id: adminRole[0].id,
})
.onConflictDoNothing();
console.log('Admin user given admin role.');
await db
.insert(schema.userRoles)
.values({
user_id: adminUser[0].id,
role_id: userRole[0].id,
})
.onConflictDoNothing();
console.log('Admin user given user role.');
await Promise.all(
users.map(async (user) => {
const [insertedUser] = await db
.insert(schema.usersTable)
.values({
...user,
hashed_password: await new Argon2id().hash(user.password),
})
.returning();
await db.insert(schema.collections).values({ user_id: insertedUser?.id });
await db.insert(schema.wishlists).values({ user_id: insertedUser?.id });
await Promise.all(
user.roles.map(async (role: JsonRole) => {
const foundRole = await db.query.roles.findFirst({
where: eq(schema.roles.name, role.name),
});
await db.insert(schema.userRoles).values({
user_id: insertedUser?.id,
role_id: foundRole?.id,
primary: role?.primary,
});
}),
);
}),
);
}

View file

@ -0,0 +1,25 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import categoriesToExternalIds from './categoriesToExternalIds';
import categories_to_games from './categoriesToGames';
import { timestamps } from '../utils';
const categories = pgTable('categories', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
name: text('name'),
slug: text('slug'),
...timestamps,
});
export type Categories = InferSelectModel<typeof categories>;
export const categories_relations = relations(categories, ({ many }) => ({
categories_to_games: many(categories_to_games),
categoriesToExternalIds: many(categoriesToExternalIds),
}));
export default categories;

View file

@ -0,0 +1,39 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import categories from './categories';
import externalIds from './externalIds';
import { relations } from 'drizzle-orm';
const categoriesToExternalIds = pgTable(
'categories_to_external_ids',
{
categoryId: uuid('category_id')
.notNull()
.references(() => categories.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(
categoriesToExternalIds,
({ one }) => ({
category: one(categories, {
fields: [categoriesToExternalIds.categoryId],
references: [categories.id],
}),
externalId: one(externalIds, {
fields: [categoriesToExternalIds.externalId],
references: [externalIds.id],
}),
}),
);
export default categoriesToExternalIds;

View file

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

View file

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

View file

@ -0,0 +1,28 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import usersTable from './users.table';
import { timestamps } from '../utils';
const collections = pgTable('collections', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
name: text('name').notNull().default('My Collection'),
...timestamps,
});
export const collection_relations = relations(collections, ({ one }) => ({
user: one(usersTable, {
fields: [collections.user_id],
references: [usersTable.id],
}),
}));
export type Collections = InferSelectModel<typeof collections>;
export default collections;

View file

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

View file

@ -0,0 +1,25 @@
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',
]);
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>;
export default externalIds;

View file

@ -0,0 +1,53 @@
import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations, sql } from 'drizzle-orm';
import categoriesToGames from './categoriesToGames';
import gamesToExternalIds from './gamesToExternalIds';
import mechanicsToGames from './mechanicsToGames';
import publishersToGames from './publishersToGames';
import { timestamps } from '../utils';
const games = pgTable(
'games',
{
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
name: text('name').notNull(),
slug: text('slug').notNull(),
description: text('description'),
year_published: integer('year_published'),
min_players: integer('min_players'),
max_players: integer('max_players'),
playtime: integer('playtime'),
min_playtime: integer('min_playtime'),
max_playtime: integer('max_playtime'),
min_age: integer('min_age'),
image_url: text('image_url'),
thumb_url: text('thumb_url'),
url: text('url'),
last_sync_at: timestamp('last_sync_at'),
...timestamps,
},
(table) => ({
searchIndex: index('search_index').using(
'gin',
sql`(
setweight(to_tsvector('english', ${table.name}), 'A') ||
setweight(to_tsvector('english', ${table.slug}), 'B')
)`,
),
}),
);
export const gameRelations = relations(games, ({ many }) => ({
categories_to_games: many(categoriesToGames),
mechanics_to_games: many(mechanicsToGames),
publishers_to_games: many(publishersToGames),
gamesToExternalIds: many(gamesToExternalIds),
}));
export type Games = InferSelectModel<typeof games>;
export default games;

View file

@ -0,0 +1,24 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import games from './games';
import externalIds from './externalIds';
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],
}),
};
},
);
export default gamesToExternalIds;

View file

@ -0,0 +1,36 @@
export { default as usersTable, userRelations as user_relations, type Users } from './users.table';
export { default as recoveryCodes, type RecoveryCodes } from './recoveryCodes';
export {
default as password_reset_tokens,
password_reset_token_relations,
type PasswordResetTokens,
} from './passwordResetTokens';
export { default as sessionsTable, type Sessions } from './sessions.table';
export { default as roles, role_relations, type Roles } from './roles';
export { default as userRoles, user_role_relations, type UserRoles } from './userRoles';
export { default as collections, collection_relations, type Collections } from './collections';
export {
default as collection_items,
collection_item_relations,
type CollectionItems,
} from './collectionItems';
export { default as wishlists, wishlists_relations, type Wishlists } from './wishlists';
export {
default as wishlist_items,
wishlist_item_relations,
type WishlistItems,
} from './wishlistItems';
export { default as externalIds, type ExternalIds, externalIdType } from './externalIds';
export { default as games, gameRelations, type Games } from './games';
export { default as gamesToExternalIds } from './gamesToExternalIds';
export { default as expansions, expansion_relations, type Expansions } from './expansions';
export { default as publishers, publishers_relations, type Publishers } from './publishers';
export { default as publishers_to_games, publishers_to_games_relations } from './publishersToGames';
export { default as publishersToExternalIds } from './publishersToExternalIds';
export { default as categories, categories_relations, type Categories } from './categories';
export { default as categoriesToExternalIds } from './categoriesToExternalIds';
export { default as categories_to_games, categories_to_games_relations } from './categoriesToGames';
export { default as mechanics, mechanics_relations, type Mechanics } from './mechanics';
export { default as mechanicsToExternalIds } from './mechanicsToExternalIds';
export { default as mechanics_to_games, mechanics_to_games_relations } from './mechanicsToGames';
export { default as twoFactor } from './two-factor.table';

View file

@ -0,0 +1,25 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import mechanicsToGames from './mechanicsToGames';
import mechanicsToExternalIds from './mechanicsToExternalIds';
import { timestamps } from '../utils';
const mechanics = 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 mechanics>;
export const mechanics_relations = relations(mechanics, ({ many }) => ({
mechanics_to_games: many(mechanicsToGames),
mechanicsToExternalIds: many(mechanicsToExternalIds),
}));
export default mechanics;

View file

@ -0,0 +1,24 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import mechanics from './mechanics';
import externalIds from './externalIds';
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],
}),
};
},
);
export default mechanicsToExternalIds;

View file

@ -0,0 +1,36 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import mechanics from './mechanics';
import games from './games';
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],
}),
}));
export default mechanics_to_games;

View file

@ -0,0 +1,27 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import usersTable from './users.table';
import { timestamps } from '../utils';
const password_reset_tokens = pgTable('password_reset_tokens', {
id: text('id')
.primaryKey()
.$defaultFn(() => cuid2()),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
expires_at: timestamp('expires_at'),
...timestamps,
});
export type PasswordResetTokens = InferSelectModel<typeof password_reset_tokens>;
export const password_reset_token_relations = relations(password_reset_tokens, ({ one }) => ({
user: one(usersTable, {
fields: [password_reset_tokens.user_id],
references: [usersTable.id],
}),
}));
export default password_reset_tokens;

View file

@ -0,0 +1,25 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import publishers_to_games from './publishersToGames';
import publishersToExternalIds from './publishersToExternalIds';
import { timestamps } from '../utils';
const publishers = 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 publishers>;
export const publishers_relations = relations(publishers, ({ many }) => ({
publishers_to_games: many(publishers_to_games),
publishersToExternalIds: many(publishersToExternalIds),
}));
export default publishers;

View file

@ -0,0 +1,24 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import publishers from './publishers';
import externalIds from './externalIds';
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],
}),
};
},
);
export default publishersToExternalIds;

View file

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

View file

@ -0,0 +1,18 @@
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import type { InferSelectModel } from 'drizzle-orm';
import usersTable from './users.table';
import { timestamps } from '../utils';
const recovery_codes = pgTable('recovery_codes', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id')
.notNull()
.references(() => usersTable.id),
code: text('code').notNull(),
used: boolean('used').default(false),
...timestamps,
});
export type RecoveryCodes = InferSelectModel<typeof recovery_codes>;
export default recovery_codes;

View file

@ -0,0 +1,23 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import user_roles from './userRoles';
import { timestamps } from '../utils';
const roles = pgTable('roles', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2())
.notNull(),
name: text('name').unique().notNull(),
...timestamps,
});
export type Roles = InferSelectModel<typeof roles>;
export const role_relations = relations(roles, ({ many }) => ({
user_roles: many(user_roles),
}));
export default roles;

View file

@ -0,0 +1,29 @@
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { relations, type InferSelectModel } from 'drizzle-orm';
import usersTable from './users.table';
const sessionsTable = pgTable('sessions', {
id: text('id').primaryKey(),
userId: uuid('user_id')
.notNull()
.references(() => usersTable.id),
expiresAt: timestamp('expires_at', {
withTimezone: true,
mode: 'date',
}).notNull(),
ipCountry: text('ip_country'),
ipAddress: text('ip_address'),
twoFactorAuthEnabled: boolean('two_factor_auth_enabled').default(false),
isTwoFactorAuthenticated: boolean('is_two_factor_authenticated').default(false),
});
export const sessionsRelations = relations(sessionsTable, ({ one }) => ({
user: one(usersTable, {
fields: [sessionsTable.userId],
references: [usersTable.id],
})
}));
export type Sessions = InferSelectModel<typeof sessionsTable>;
export default sessionsTable;

View file

@ -0,0 +1,34 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../utils';
import usersTable from './users.table';
const twoFactorTable = pgTable('two_factor', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
secret: text('secret').notNull(),
enabled: boolean('enabled').notNull().default(false),
initiatedTime: timestamp('initiated_time', {
mode: 'date',
withTimezone: true,
}),
userId: uuid('user_id')
.notNull()
.references(() => usersTable.id)
.unique(),
...timestamps,
});
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
user: one(usersTable, {
fields: [twoFactorTable.userId],
references: [usersTable.id],
}),
}));
export type TwoFactor = InferSelectModel<typeof twoFactorTable>;
export default twoFactorTable;

View file

@ -0,0 +1,36 @@
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import usersTable from './users.table';
import roles from './roles';
import { timestamps } from '../utils';
const user_roles = pgTable('user_roles', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
role_id: uuid('role_id')
.notNull()
.references(() => roles.id, { onDelete: 'cascade' }),
primary: boolean('primary').default(false),
...timestamps,
});
export const user_role_relations = relations(user_roles, ({ one }) => ({
role: one(roles, {
fields: [user_roles.role_id],
references: [roles.id],
}),
user: one(usersTable, {
fields: [user_roles.user_id],
references: [usersTable.id],
}),
}));
export type UserRoles = InferSelectModel<typeof user_roles>;
export default user_roles;

View file

@ -0,0 +1,29 @@
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { timestamps } from '../utils';
import user_roles from './userRoles';
const usersTable = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
username: text('username').unique(),
hashed_password: text('hashed_password'),
email: text('email').unique(),
first_name: text('first_name'),
last_name: text('last_name'),
verified: boolean('verified').default(false),
receive_email: boolean('receive_email').default(false),
theme: text('theme').default('system'),
...timestamps,
});
export const userRelations = relations(usersTable, ({ many }) => ({
user_roles: many(user_roles),
}));
export type Users = InferSelectModel<typeof usersTable>;
export default usersTable;

View file

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

View file

@ -0,0 +1,28 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import usersTable from './users.table';
import { timestamps } from '../utils';
const wishlists = pgTable('wishlists', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
name: text('name').notNull().default('My Wishlist'),
...timestamps,
});
export type Wishlists = InferSelectModel<typeof wishlists>;
export const wishlists_relations = relations(wishlists, ({ one }) => ({
user: one(usersTable, {
fields: [wishlists.user_id],
references: [usersTable.id],
}),
}));
export default wishlists;

View file

@ -0,0 +1,42 @@
import { HTTPException } from 'hono/http-exception';
import { timestamp, customType } from 'drizzle-orm/pg-core';
export const citext = customType<{ data: string }>({
dataType() {
return 'citext';
}
});
export const cuid2 = customType<{ data: string }>({
dataType() {
return 'text';
}
});
export const takeFirst = <T>(values: T[]): T | null => {
if (values.length === 0) return null;
return values[0]!;
};
export const takeFirstOrThrow = <T>(values: T[]): T => {
if (values.length === 0)
throw new HTTPException(404, {
message: 'Resource not found'
});
return values[0]!;
};
export const timestamps = {
createdAt: timestamp('created_at', {
mode: 'date',
withTimezone: true
})
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', {
mode: 'date',
withTimezone: true
})
.notNull()
.defaultNow()
};

View file

@ -0,0 +1,18 @@
<html lang='en'>
<head>
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>Email Change Request</title>
</head>
<body>
<p class='title'>Email address change notice </p>
<p>
An update to your email address has been requested. If this is unexpected or you did not perform this action, please login and secure your account.</p>
</body>
<style>
.title { font-size: 24px; font-weight: 700; } .token-text { font-size: 24px; font-weight: 700; margin-top: 8px; }
.token-title { font-size: 18px; font-weight: 700; margin-bottom: 0px; }
.center { display: flex; justify-content: center; align-items: center; flex-direction: column;}
.token-subtext { font-size: 12px; margin-top: 0px; }
</style>
</html>

View file

@ -0,0 +1,10 @@
import { db } from '../infrastructure/database';
// Symbol
export const DatabaseProvider = Symbol('DATABASE_TOKEN');
// Type
export type DatabaseProvider = typeof db;
// Register
container.register<DatabaseProvider>(DatabaseProvider, { useValue: db });

View file

@ -0,0 +1,3 @@
export * from './database.provider';
export * from './lucia.provider';
export * from './redis.provider';

View file

@ -0,0 +1,11 @@
import { container } from 'tsyringe';
import { lucia } from '../infrastructure/auth/lucia';
// Symbol
export const LuciaProvider = Symbol('LUCIA_PROVIDER');
// Type
export type LuciaProvider = typeof lucia;
// Register
container.register<LuciaProvider>(LuciaProvider, { useValue: lucia });

View file

@ -0,0 +1,14 @@
import { container } from 'tsyringe';
import RedisClient from 'ioredis'
import { config } from '../common/config';
// Symbol
export const RedisProvider = Symbol('REDIS_TOKEN');
// Type
export type RedisProvider = RedisClient;
// Register
container.register<RedisProvider>(RedisProvider, {
useValue: new RedisClient(config.REDIS_URL)
});

View file

@ -0,0 +1,62 @@
import { BadRequest } from '../common/errors';
import { DatabaseProvider } from '../providers';
import { MailerService } from './mailer.service';
import { TokensService } from './tokens.service';
import { LuciaProvider } from '../providers/lucia.provider';
import { UsersRepository } from '../repositories/users.repository';
import type { SignInEmailDto } from '../../../dtos/signin-email.dto';
import type { RegisterEmailDto } from '../../../dtos/register-email.dto';
import { LoginRequestsRepository } from '../repositories/login-requests.repository';
export class LoginRequestsService {
async create(data: RegisterEmailDto) {
// generate a token, expiry date, and hash
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm');
// save the login request to the database - ensuring we save the hashedToken
await this.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry });
// send the login request email
await this.mailerService.sendLoginRequest({
to: data.email,
props: { token: token }
});
}
async verify(data: SignInEmailDto) {
const validLoginRequest = await this.fetchValidRequest(data.email, data.token);
if (!validLoginRequest) throw BadRequest('Invalid token');
let existingUser = await this.usersRepository.findOneByEmail(data.email);
if (!existingUser) {
const newUser = await this.handleNewUserRegistration(data.email);
return this.lucia.createSession(newUser.id, {});
}
return this.lucia.createSession(existingUser.id, {});
}
// Create a new user and send a welcome email - or other onboarding process
private async handleNewUserRegistration(email: string) {
const newUser = await this.usersRepository.create({ email, verified: true, avatar: null })
this.mailerService.sendWelcome({ to: email, props: null });
// TODO: add whatever onboarding process or extra data you need here
return newUser
}
// Fetch a valid request from the database, verify the token and burn the request if it is valid
private async fetchValidRequest(email: string, token: string) {
return await this.db.transaction(async (trx) => {
// fetch the login request
const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email)
if (!loginRequest) return null;
// check if the token is valid
const isValidRequest = await this.tokensService.verifyHashedToken(loginRequest.hashedToken, token);
if (!isValidRequest) return null
// if the token is valid, burn the request
await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id);
return loginRequest
})
}
}

View file

@ -9,7 +9,10 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
"strict": true,
"moduleResolution": "bundler",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//