mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Moving a lot around for hono
This commit is contained in:
parent
388f9a399d
commit
d70b3061b5
25 changed files with 999 additions and 59 deletions
|
|
@ -77,8 +77,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/fira-mono": "^5.0.13",
|
"@fontsource/fira-mono": "^5.0.13",
|
||||||
|
"@hono/zod-validator": "^0.2.2",
|
||||||
"@iconify-icons/line-md": "^1.2.30",
|
"@iconify-icons/line-md": "^1.2.30",
|
||||||
"@iconify-icons/mdi": "^1.2.48",
|
"@iconify-icons/mdi": "^1.2.48",
|
||||||
|
"@internationalized/date": "^3.5.4",
|
||||||
"@lucia-auth/adapter-drizzle": "^1.0.7",
|
"@lucia-auth/adapter-drizzle": "^1.0.7",
|
||||||
"@lukeed/uuid": "^2.0.1",
|
"@lukeed/uuid": "^2.0.1",
|
||||||
"@neondatabase/serverless": "^0.9.4",
|
"@neondatabase/serverless": "^0.9.4",
|
||||||
|
|
@ -97,8 +99,11 @@
|
||||||
"drizzle-orm": "^0.32.0",
|
"drizzle-orm": "^0.32.0",
|
||||||
"feather-icons": "^4.29.2",
|
"feather-icons": "^4.29.2",
|
||||||
"formsnap": "^1.0.1",
|
"formsnap": "^1.0.1",
|
||||||
|
"hono": "^4.5.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",
|
||||||
|
"ioredis": "^5.4.1",
|
||||||
"just-capitalize": "^3.2.0",
|
"just-capitalize": "^3.2.0",
|
||||||
"just-kebab-case": "^4.2.0",
|
"just-kebab-case": "^4.2.0",
|
||||||
"loader": "^2.1.1",
|
"loader": "^2.1.1",
|
||||||
|
|
@ -110,6 +115,8 @@
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"radix-svelte": "^0.9.0",
|
"radix-svelte": "^0.9.0",
|
||||||
|
"rate-limit-redis": "^4.2.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
"svelte-french-toast": "^1.2.0",
|
"svelte-french-toast": "^1.2.0",
|
||||||
"svelte-lazy-loader": "^1.0.0",
|
"svelte-lazy-loader": "^1.0.0",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
|
|
|
||||||
637
pnpm-lock.yaml
637
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
21
src/app.d.ts
vendored
21
src/app.d.ts
vendored
|
|
@ -1,3 +1,7 @@
|
||||||
|
import { ApiClient } from './lib/server/api';
|
||||||
|
import type { User } from 'lucia';
|
||||||
|
import { parseApiResponse } from '$lib/utils/api';
|
||||||
|
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
// and what to do when importing types
|
// and what to do when importing types
|
||||||
|
|
@ -13,6 +17,10 @@ declare global {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
|
api: ApiClient['api'];
|
||||||
|
parseApiResponse: typeof parseApiResponse;
|
||||||
|
getAuthedUser: () => Promise<Returned<User> | null>;
|
||||||
|
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
||||||
auth: import('lucia').AuthRequest;
|
auth: import('lucia').AuthRequest;
|
||||||
user: import('lucia').User | null;
|
user: import('lucia').User | null;
|
||||||
session: import('lucia').Session | null;
|
session: import('lucia').Session | null;
|
||||||
|
|
@ -37,18 +45,5 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// interface PageData {}
|
|
||||||
// interface Error {}
|
|
||||||
// interface Platform {}
|
|
||||||
|
|
||||||
// /// <reference types="lucia" />
|
|
||||||
// declare global {
|
|
||||||
// namespace Lucia {
|
|
||||||
// type Auth = import('$lib/server/lucia').Auth;
|
|
||||||
// type DatabaseUserAttributes = User;
|
|
||||||
// type DatabaseSessionAttributes = {};
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// THIS IS IMPORTANT!!!
|
// THIS IS IMPORTANT!!!
|
||||||
export {};
|
export {};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
|
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
|
||||||
import categories from './categories';
|
import categories from './categories';
|
||||||
import externalIds from './externalIds';
|
import externalIds from './externalIds';
|
||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
|
||||||
const categoriesToExternalIds = pgTable(
|
const categoriesToExternalIds = pgTable(
|
||||||
'categories_to_external_ids',
|
'categories_to_external_ids',
|
||||||
|
|
@ -21,4 +22,18 @@ const categoriesToExternalIds = pgTable(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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;
|
export default categoriesToExternalIds;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||||
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 users from './users';
|
import usersTable from './users.table';
|
||||||
import { timestamps } from '../utils';
|
import { timestamps } from '../utils';
|
||||||
|
|
||||||
const collections = pgTable('collections', {
|
const collections = pgTable('collections', {
|
||||||
|
|
@ -11,15 +11,15 @@ const collections = pgTable('collections', {
|
||||||
.$defaultFn(() => cuid2()),
|
.$defaultFn(() => cuid2()),
|
||||||
user_id: uuid('user_id')
|
user_id: uuid('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: 'cascade' }),
|
.references(() => usersTable.id, { onDelete: 'cascade' }),
|
||||||
name: text('name').notNull().default('My Collection'),
|
name: text('name').notNull().default('My Collection'),
|
||||||
...timestamps,
|
...timestamps,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collection_relations = relations(collections, ({ one }) => ({
|
export const collection_relations = relations(collections, ({ one }) => ({
|
||||||
user: one(users, {
|
user: one(usersTable, {
|
||||||
fields: [collections.user_id],
|
fields: [collections.user_id],
|
||||||
references: [users.id],
|
references: [usersTable.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
export { default as users, user_relations, type Users } from './users';
|
export { default as usersTable, userRelations as user_relations, type Users } from './users.table';
|
||||||
export { default as recoveryCodes, type RecoveryCodes } from './recoveryCodes';
|
export { default as recoveryCodes, type RecoveryCodes } from './recoveryCodes';
|
||||||
export {
|
export {
|
||||||
default as password_reset_tokens,
|
default as password_reset_tokens,
|
||||||
password_reset_token_relations,
|
password_reset_token_relations,
|
||||||
type PasswordResetTokens,
|
type PasswordResetTokens,
|
||||||
} from './passwordResetTokens';
|
} from './passwordResetTokens';
|
||||||
export { default as sessions, type Sessions } from './sessions';
|
export { default as sessionsTable, type Sessions } from './sessions.table';
|
||||||
export { default as roles, role_relations, type Roles } from './roles';
|
export { default as roles, role_relations, type Roles } from './roles';
|
||||||
export { default as userRoles, user_role_relations, type UserRoles } from './userRoles';
|
export { default as userRoles, user_role_relations, type UserRoles } from './userRoles';
|
||||||
export { default as collections, collection_relations, type Collections } from './collections';
|
export { default as collections, collection_relations, type Collections } from './collections';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||||
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 users from './users';
|
import usersTable from './users.table';
|
||||||
import { timestamps } from '../utils';
|
import { timestamps } from '../utils';
|
||||||
|
|
||||||
const password_reset_tokens = pgTable('password_reset_tokens', {
|
const password_reset_tokens = pgTable('password_reset_tokens', {
|
||||||
|
|
@ -10,7 +10,7 @@ const password_reset_tokens = pgTable('password_reset_tokens', {
|
||||||
.$defaultFn(() => cuid2()),
|
.$defaultFn(() => cuid2()),
|
||||||
user_id: uuid('user_id')
|
user_id: uuid('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: 'cascade' }),
|
.references(() => usersTable.id, { onDelete: 'cascade' }),
|
||||||
expires_at: timestamp('expires_at'),
|
expires_at: timestamp('expires_at'),
|
||||||
...timestamps,
|
...timestamps,
|
||||||
});
|
});
|
||||||
|
|
@ -18,9 +18,9 @@ const password_reset_tokens = pgTable('password_reset_tokens', {
|
||||||
export type PasswordResetTokens = InferSelectModel<typeof password_reset_tokens>;
|
export type PasswordResetTokens = 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(users, {
|
user: one(usersTable, {
|
||||||
fields: [password_reset_tokens.user_id],
|
fields: [password_reset_tokens.user_id],
|
||||||
references: [users.id],
|
references: [usersTable.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||||
import type { InferSelectModel } from 'drizzle-orm';
|
import type { InferSelectModel } from 'drizzle-orm';
|
||||||
import users from './users';
|
import usersTable from './users.table';
|
||||||
import { timestamps } from '../utils';
|
import { timestamps } from '../utils';
|
||||||
|
|
||||||
const recovery_codes = pgTable('recovery_codes', {
|
const recovery_codes = pgTable('recovery_codes', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
userId: uuid('user_id')
|
userId: uuid('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => usersTable.id),
|
||||||
code: text('code').notNull(),
|
code: text('code').notNull(),
|
||||||
used: boolean('used').default(false),
|
used: boolean('used').default(false),
|
||||||
...timestamps,
|
...timestamps,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||||
import { type InferSelectModel } from 'drizzle-orm';
|
import { relations, type InferSelectModel } from 'drizzle-orm';
|
||||||
import users from './users';
|
import usersTable from './users.table';
|
||||||
|
|
||||||
const sessions = pgTable('sessions', {
|
const sessionsTable = pgTable('sessions', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
userId: uuid('user_id')
|
userId: uuid('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => usersTable.id),
|
||||||
expiresAt: timestamp('expires_at', {
|
expiresAt: timestamp('expires_at', {
|
||||||
withTimezone: true,
|
withTimezone: true,
|
||||||
mode: 'date',
|
mode: 'date',
|
||||||
|
|
@ -17,6 +17,13 @@ const sessions = pgTable('sessions', {
|
||||||
isTwoFactorAuthenticated: boolean('is_two_factor_authenticated').default(false),
|
isTwoFactorAuthenticated: boolean('is_two_factor_authenticated').default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Sessions = InferSelectModel<typeof sessions>;
|
export const sessionsRelations = relations(sessionsTable, ({ one }) => ({
|
||||||
|
user: one(usersTable, {
|
||||||
|
fields: [sessionsTable.userId],
|
||||||
|
references: [usersTable.id],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
export default sessions;
|
export type Sessions = InferSelectModel<typeof sessionsTable>;
|
||||||
|
|
||||||
|
export default sessionsTable;
|
||||||
|
|
@ -2,7 +2,7 @@ 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 '../utils';
|
import { timestamps } from '../utils';
|
||||||
import users from './users';
|
import usersTable from './users.table';
|
||||||
|
|
||||||
const twoFactorTable = pgTable('two_factor', {
|
const twoFactorTable = pgTable('two_factor', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
|
@ -17,15 +17,15 @@ const twoFactorTable = pgTable('two_factor', {
|
||||||
}),
|
}),
|
||||||
userId: uuid('user_id')
|
userId: uuid('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id)
|
.references(() => usersTable.id)
|
||||||
.unique(),
|
.unique(),
|
||||||
...timestamps,
|
...timestamps,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
|
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
|
||||||
user: one(users, {
|
user: one(usersTable, {
|
||||||
fields: [twoFactorTable.userId],
|
fields: [twoFactorTable.userId],
|
||||||
references: [users.id],
|
references: [usersTable.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||||
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 users from './users';
|
import usersTable from './users.table';
|
||||||
import roles from './roles';
|
import roles from './roles';
|
||||||
import { timestamps } from '../utils';
|
import { timestamps } from '../utils';
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ const user_roles = pgTable('user_roles', {
|
||||||
.$defaultFn(() => cuid2()),
|
.$defaultFn(() => cuid2()),
|
||||||
user_id: uuid('user_id')
|
user_id: uuid('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.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(() => roles.id, { onDelete: 'cascade' }),
|
||||||
|
|
@ -25,9 +25,9 @@ export const user_role_relations = relations(user_roles, ({ one }) => ({
|
||||||
fields: [user_roles.role_id],
|
fields: [user_roles.role_id],
|
||||||
references: [roles.id],
|
references: [roles.id],
|
||||||
}),
|
}),
|
||||||
user: one(users, {
|
user: one(usersTable, {
|
||||||
fields: [user_roles.user_id],
|
fields: [user_roles.user_id],
|
||||||
references: [users.id],
|
references: [usersTable.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||||
import { timestamps } from '../utils';
|
import { timestamps } from '../utils';
|
||||||
import user_roles from './userRoles';
|
import user_roles from './userRoles';
|
||||||
|
|
||||||
const users = pgTable('users', {
|
const usersTable = pgTable('users', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
cuid: text('cuid')
|
cuid: text('cuid')
|
||||||
.unique()
|
.unique()
|
||||||
|
|
@ -20,10 +20,10 @@ const users = pgTable('users', {
|
||||||
...timestamps,
|
...timestamps,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const user_relations = relations(users, ({ many }) => ({
|
export const userRelations = relations(usersTable, ({ many }) => ({
|
||||||
user_roles: many(user_roles),
|
user_roles: many(user_roles),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type Users = InferSelectModel<typeof users>;
|
export type Users = InferSelectModel<typeof usersTable>;
|
||||||
|
|
||||||
export default users;
|
export default usersTable;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||||
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 users from './users';
|
import usersTable from './users.table';
|
||||||
import { timestamps } from '../utils';
|
import { timestamps } from '../utils';
|
||||||
|
|
||||||
const wishlists = pgTable('wishlists', {
|
const wishlists = pgTable('wishlists', {
|
||||||
|
|
@ -11,7 +11,7 @@ const wishlists = pgTable('wishlists', {
|
||||||
.$defaultFn(() => cuid2()),
|
.$defaultFn(() => cuid2()),
|
||||||
user_id: uuid('user_id')
|
user_id: uuid('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: 'cascade' }),
|
.references(() => usersTable.id, { onDelete: 'cascade' }),
|
||||||
name: text('name').notNull().default('My Wishlist'),
|
name: text('name').notNull().default('My Wishlist'),
|
||||||
...timestamps,
|
...timestamps,
|
||||||
});
|
});
|
||||||
|
|
@ -19,9 +19,9 @@ const wishlists = pgTable('wishlists', {
|
||||||
export type Wishlists = InferSelectModel<typeof wishlists>;
|
export type Wishlists = InferSelectModel<typeof wishlists>;
|
||||||
|
|
||||||
export const wishlists_relations = relations(wishlists, ({ one }) => ({
|
export const wishlists_relations = relations(wishlists, ({ one }) => ({
|
||||||
user: one(users, {
|
user: one(usersTable, {
|
||||||
fields: [wishlists.user_id],
|
fields: [wishlists.user_id],
|
||||||
references: [users.id],
|
references: [usersTable.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
// import * as Sentry from '@sentry/sveltekit';
|
// import * as Sentry from '@sentry/sveltekit';
|
||||||
|
import { hc } from 'hono/client';
|
||||||
import { sequence } from '@sveltejs/kit/hooks';
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
import type { Handle } from '@sveltejs/kit';
|
import { redirect, type Handle } from '@sveltejs/kit';
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { lucia } from '$lib/server/auth';
|
import { lucia } from '$lib/server/auth';
|
||||||
|
import type { ApiRoutes } from '$lib/server/api';
|
||||||
|
import { parseApiResponse } from '$lib/utils/api';
|
||||||
|
import { StatusCodes } from '$lib/constants/status-codes';
|
||||||
|
|
||||||
// TODO: Fix Sentry as it is not working on SvelteKit v2
|
// TODO: Fix Sentry as it is not working on SvelteKit v2
|
||||||
// Sentry.init({
|
// Sentry.init({
|
||||||
|
|
@ -12,6 +16,39 @@ import { lucia } from '$lib/server/auth';
|
||||||
// enabled: !dev
|
// enabled: !dev
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
const apiClient: Handle = async ({ event, resolve }) => {
|
||||||
|
/* ------------------------------ Register api ------------------------------ */
|
||||||
|
const { api } = hc<ApiRoutes>('/', {
|
||||||
|
fetch: event.fetch,
|
||||||
|
headers: {
|
||||||
|
'x-forwarded-for': event.getClientAddress(),
|
||||||
|
host: event.request.headers.get('host') || ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ----------------------------- Auth functions ----------------------------- */
|
||||||
|
async function getAuthedUser() {
|
||||||
|
const { data } = await api.iam.user.$get().then(parseApiResponse)
|
||||||
|
return data && data.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuthedUserOrThrow() {
|
||||||
|
const { data } = await api.iam.user.$get().then(parseApiResponse);
|
||||||
|
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
|
||||||
|
return data?.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------ Set contexts ------------------------------ */
|
||||||
|
event.locals.api = api;
|
||||||
|
event.locals.parseApiResponse = parseApiResponse;
|
||||||
|
event.locals.getAuthedUser = getAuthedUser;
|
||||||
|
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow;
|
||||||
|
|
||||||
|
/* ----------------------------- Return response ---------------------------- */
|
||||||
|
const response = await resolve(event);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
export const authentication: Handle = async function ({ event, resolve }) {
|
export const authentication: Handle = async function ({ event, resolve }) {
|
||||||
event.locals.startTimer = Date.now();
|
event.locals.startTimer = Date.now();
|
||||||
|
|
||||||
|
|
@ -55,5 +92,6 @@ export const authentication: Handle = async function ({ event, resolve }) {
|
||||||
export const handle: Handle = sequence(
|
export const handle: Handle = sequence(
|
||||||
// Sentry.sentryHandle(),
|
// Sentry.sentryHandle(),
|
||||||
authentication,
|
authentication,
|
||||||
|
apiClient
|
||||||
);
|
);
|
||||||
// export const handleError = Sentry.handleErrorWithSentry();
|
// export const handleError = Sentry.handleErrorWithSentry();
|
||||||
|
|
|
||||||
14
src/lib/server/api/common/config.ts
Normal file
14
src/lib/server/api/common/config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import * as envs from '$env/static/private';
|
||||||
|
|
||||||
|
const isPreview = process.env.VERCEL_ENV === 'preview' || process.env.VERCEL_ENV === 'development';
|
||||||
|
|
||||||
|
let domain;
|
||||||
|
if (process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production') {
|
||||||
|
domain = 'boredgame.vercel.app';
|
||||||
|
} else if (isPreview) {
|
||||||
|
domain = process.env.VERCEL_BRANCH_URL;
|
||||||
|
} else {
|
||||||
|
domain = 'localhost';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = { ...envs, isProduction: process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production', domain };
|
||||||
26
src/lib/server/api/common/errors.ts
Normal file
26
src/lib/server/api/common/errors.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { StatusCodes } from '$lib/constants/status-codes';
|
||||||
|
import { HTTPException } from 'hono/http-exception';
|
||||||
|
|
||||||
|
export function TooManyRequests(message: string = 'Too many requests') {
|
||||||
|
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Forbidden(message: string = 'Forbidden') {
|
||||||
|
return new HTTPException(StatusCodes.FORBIDDEN, { message });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Unauthorized(message: string = 'Unauthorized') {
|
||||||
|
return new HTTPException(StatusCodes.UNAUTHORIZED, { message });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotFound(message: string = 'Not Found') {
|
||||||
|
return new HTTPException(StatusCodes.NOT_FOUND, { message });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BadRequest(message: string = 'Bad Request') {
|
||||||
|
return new HTTPException(StatusCodes.BAD_REQUEST, { message });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InternalError(message: string = 'Internal Error') {
|
||||||
|
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message });
|
||||||
|
}
|
||||||
10
src/lib/server/api/controllers/iam.controller.ts
Normal file
10
src/lib/server/api/controllers/iam.controller.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import { requireAuth } from "../middleware/auth.middleware";
|
||||||
|
|
||||||
|
const users = new Hono().get('/me', requireAuth, async (c) => {
|
||||||
|
const user = c.var.user;
|
||||||
|
return c.json({ user });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default users;
|
||||||
|
export type UsersType = typeof users
|
||||||
23
src/lib/server/api/index.ts
Normal file
23
src/lib/server/api/index.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import { hc } from 'hono/client';
|
||||||
|
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware';
|
||||||
|
import users from './controllers/iam.controller';
|
||||||
|
import { config } from './common/config';
|
||||||
|
|
||||||
|
/* ----------------------------------- Api ---------------------------------- */
|
||||||
|
const app = new Hono().basePath('/api');
|
||||||
|
|
||||||
|
/* --------------------------- Global Middlewares --------------------------- */
|
||||||
|
app.use(verifyOrigin).use(validateAuthSession);
|
||||||
|
|
||||||
|
/* --------------------------------- Routes --------------------------------- */
|
||||||
|
const routes = app
|
||||||
|
.route('/iam', users)
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Exports */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
export const rpc = hc<typeof routes>(config.ORIGIN);
|
||||||
|
export type ApiClient = typeof rpc;
|
||||||
|
export type ApiRoutes = typeof routes;
|
||||||
|
export { app };
|
||||||
56
src/lib/server/api/infrastructure/auth/lucia.ts
Normal file
56
src/lib/server/api/infrastructure/auth/lucia.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
// lib/server/lucia.ts
|
||||||
|
import { Lucia, TimeSpan } from 'lucia';
|
||||||
|
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle';
|
||||||
|
import db from '$db/index';
|
||||||
|
import { sessionsTable, usersTable } from '$db/schema';
|
||||||
|
import { config } from '../../common/config';
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sessionExpiresIn: new TimeSpan(30, 'd'), // 30 days
|
||||||
|
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;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/lib/server/api/middleware/auth.middleware.ts
Normal file
50
src/lib/server/api/middleware/auth.middleware.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import type { MiddlewareHandler } from 'hono';
|
||||||
|
import { createMiddleware } from 'hono/factory';
|
||||||
|
import type { HonoTypes } from '../types';
|
||||||
|
import { lucia } from '../infrastructure/auth/lucia';
|
||||||
|
import { verifyRequestOrigin } from 'lucia';
|
||||||
|
import type { Session, User } from 'lucia';
|
||||||
|
import { Unauthorized } from '../common/errors';
|
||||||
|
|
||||||
|
export const verifyOrigin: MiddlewareHandler<HonoTypes> = createMiddleware(async (c, next) => {
|
||||||
|
if (c.req.method === "GET") {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
const originHeader = c.req.header("Origin") ?? null;
|
||||||
|
const hostHeader = c.req.header("Host") ?? null;
|
||||||
|
if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) {
|
||||||
|
return c.body(null, 403);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
})
|
||||||
|
|
||||||
|
export const validateAuthSession: MiddlewareHandler<HonoTypes> = createMiddleware(async (c, next) => {
|
||||||
|
const sessionId = lucia.readSessionCookie(c.req.header("Cookie") ?? "");
|
||||||
|
if (!sessionId) {
|
||||||
|
c.set("user", null);
|
||||||
|
c.set("session", null);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session, user } = await lucia.validateSession(sessionId);
|
||||||
|
if (session && session.fresh) {
|
||||||
|
c.header("Set-Cookie", lucia.createSessionCookie(session.id).serialize(), { append: true });
|
||||||
|
}
|
||||||
|
if (!session) {
|
||||||
|
c.header("Set-Cookie", lucia.createBlankSessionCookie().serialize(), { append: true });
|
||||||
|
}
|
||||||
|
c.set("session", session);
|
||||||
|
c.set("user", user);
|
||||||
|
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();
|
||||||
|
});
|
||||||
32
src/lib/server/api/middleware/rate-limiter.middleware.ts
Normal file
32
src/lib/server/api/middleware/rate-limiter.middleware.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { rateLimiter } from "hono-rate-limiter";
|
||||||
|
import { RedisStore } from 'rate-limit-redis'
|
||||||
|
import RedisClient from 'ioredis'
|
||||||
|
import type { HonoTypes } from "../types";
|
||||||
|
import { config } from "../common/config";
|
||||||
|
|
||||||
|
const client = new RedisClient(config.REDIS_URL)
|
||||||
|
|
||||||
|
export function limiter({ limit, minutes, key = "" }: {
|
||||||
|
limit: number;
|
||||||
|
minutes: number;
|
||||||
|
key?: string;
|
||||||
|
}) {
|
||||||
|
return rateLimiter({
|
||||||
|
windowMs: minutes * 60 * 1000, // every x minutes
|
||||||
|
limit, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
|
||||||
|
standardHeaders: "draft-6", // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
|
||||||
|
keyGenerator: (c) => {
|
||||||
|
const vars = c.var as HonoTypes['Variables'];
|
||||||
|
const clientKey = vars.user?.id || c.req.header("x-forwarded-for");
|
||||||
|
const pathKey = key || c.req.routePath;
|
||||||
|
return `${clientKey}_${pathKey}`
|
||||||
|
}, // Method to generate custom identifiers for clients.
|
||||||
|
// Redis store configuration
|
||||||
|
store: new RedisStore({
|
||||||
|
// @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis
|
||||||
|
sendCommand: (...args: string[]) => client.call(...args),
|
||||||
|
}) as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
14
src/lib/server/api/types/index.ts
Normal file
14
src/lib/server/api/types/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Promisify, RateLimitInfo } from 'hono-rate-limiter';
|
||||||
|
import type { Session, User } from 'lucia';
|
||||||
|
|
||||||
|
export type HonoTypes = {
|
||||||
|
Variables: {
|
||||||
|
session: Session | null;
|
||||||
|
user: User | null;
|
||||||
|
rateLimit: RateLimitInfo;
|
||||||
|
rateLimitStore: {
|
||||||
|
getKey?: (key: string) => Promisify<RateLimitInfo | undefined>;
|
||||||
|
resetKey: (key: string) => Promisify<void>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
import { Lucia, TimeSpan } from 'lucia';
|
import { Lucia, TimeSpan } from 'lucia';
|
||||||
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle';
|
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle';
|
||||||
import db from '../../db';
|
import db from '../../db';
|
||||||
import { sessions, users } from '$db/schema';
|
import { sessionsTable, usersTable } from '$db/schema';
|
||||||
|
|
||||||
const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);
|
const adapter = new DrizzlePostgreSQLAdapter(db, sessionsTable, usersTable);
|
||||||
|
|
||||||
let domain;
|
let domain;
|
||||||
if (process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production') {
|
||||||
|
|
|
||||||
25
src/lib/utils/api.ts
Normal file
25
src/lib/utils/api.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type { ClientResponse } from "hono/client";
|
||||||
|
|
||||||
|
export async function parseApiResponse<T>(response: ClientResponse<T>) {
|
||||||
|
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
|
||||||
|
return response.ok
|
||||||
|
? { data: null, error: null, response }
|
||||||
|
: { data: null, error: 'An unknown error has occured', response };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json() as T;
|
||||||
|
|
||||||
|
return { data, error: null, status: response.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle errors
|
||||||
|
let error = await response.text();
|
||||||
|
try {
|
||||||
|
error = JSON.parse(error); // attempt to parse as JSON
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
return { data: null, error, response };
|
||||||
|
}
|
||||||
|
return { data: null, error, response };
|
||||||
|
}
|
||||||
9
src/routes/api/[...slug]/+server.ts
Normal file
9
src/routes/api/[...slug]/+server.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { app } from '$lib/server/api';
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = ({ request }) => app.request(request);
|
||||||
|
export const PUT: RequestHandler = ({ request }) => app.request(request);
|
||||||
|
export const DELETE: RequestHandler = ({ request }) => app.fetch(request);
|
||||||
|
export const POST: RequestHandler = ({ request }) => app.fetch(request);
|
||||||
|
export const PATCH: RequestHandler = ({ request }) => app.fetch(request);
|
||||||
|
export const fallback: RequestHandler = ({ request }) => app.fetch(request);
|
||||||
Loading…
Reference in a new issue