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": {
|
||||
"@fontsource/fira-mono": "^5.0.13",
|
||||
"@hono/zod-validator": "^0.2.2",
|
||||
"@iconify-icons/line-md": "^1.2.30",
|
||||
"@iconify-icons/mdi": "^1.2.48",
|
||||
"@internationalized/date": "^3.5.4",
|
||||
"@lucia-auth/adapter-drizzle": "^1.0.7",
|
||||
"@lukeed/uuid": "^2.0.1",
|
||||
"@neondatabase/serverless": "^0.9.4",
|
||||
|
|
@ -97,8 +99,11 @@
|
|||
"drizzle-orm": "^0.32.0",
|
||||
"feather-icons": "^4.29.2",
|
||||
"formsnap": "^1.0.1",
|
||||
"hono": "^4.5.0",
|
||||
"hono-rate-limiter": "^0.4.0",
|
||||
"html-entities": "^2.5.2",
|
||||
"iconify-icon": "^2.1.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"just-capitalize": "^3.2.0",
|
||||
"just-kebab-case": "^4.2.0",
|
||||
"loader": "^2.1.1",
|
||||
|
|
@ -110,6 +115,8 @@
|
|||
"postgres": "^3.4.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"radix-svelte": "^0.9.0",
|
||||
"rate-limit-redis": "^4.2.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"svelte-french-toast": "^1.2.0",
|
||||
"svelte-lazy-loader": "^1.0.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
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
|
|
@ -13,6 +17,10 @@ declare global {
|
|||
};
|
||||
}
|
||||
interface Locals {
|
||||
api: ApiClient['api'];
|
||||
parseApiResponse: typeof parseApiResponse;
|
||||
getAuthedUser: () => Promise<Returned<User> | null>;
|
||||
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
||||
auth: import('lucia').AuthRequest;
|
||||
user: import('lucia').User | 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!!!
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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',
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
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 users from './users';
|
||||
import usersTable from './users.table';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const collections = pgTable('collections', {
|
||||
|
|
@ -11,15 +11,15 @@ const collections = pgTable('collections', {
|
|||
.$defaultFn(() => cuid2()),
|
||||
user_id: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
.references(() => usersTable.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull().default('My Collection'),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const collection_relations = relations(collections, ({ one }) => ({
|
||||
user: one(users, {
|
||||
user: one(usersTable, {
|
||||
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 password_reset_tokens,
|
||||
password_reset_token_relations,
|
||||
type 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 userRoles, user_role_relations, type UserRoles } from './userRoles';
|
||||
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 { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import users from './users';
|
||||
import usersTable from './users.table';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const password_reset_tokens = pgTable('password_reset_tokens', {
|
||||
|
|
@ -10,7 +10,7 @@ const password_reset_tokens = pgTable('password_reset_tokens', {
|
|||
.$defaultFn(() => cuid2()),
|
||||
user_id: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
.references(() => usersTable.id, { onDelete: 'cascade' }),
|
||||
expires_at: timestamp('expires_at'),
|
||||
...timestamps,
|
||||
});
|
||||
|
|
@ -18,9 +18,9 @@ const password_reset_tokens = pgTable('password_reset_tokens', {
|
|||
export type PasswordResetTokens = InferSelectModel<typeof password_reset_tokens>;
|
||||
|
||||
export const password_reset_token_relations = relations(password_reset_tokens, ({ one }) => ({
|
||||
user: one(users, {
|
||||
user: one(usersTable, {
|
||||
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 type { InferSelectModel } from 'drizzle-orm';
|
||||
import users from './users';
|
||||
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(() => users.id),
|
||||
.references(() => usersTable.id),
|
||||
code: text('code').notNull(),
|
||||
used: boolean('used').default(false),
|
||||
...timestamps,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
import { type InferSelectModel } from 'drizzle-orm';
|
||||
import users from './users';
|
||||
import { relations, type InferSelectModel } from 'drizzle-orm';
|
||||
import usersTable from './users.table';
|
||||
|
||||
const sessions = pgTable('sessions', {
|
||||
const sessionsTable = pgTable('sessions', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
.references(() => usersTable.id),
|
||||
expiresAt: timestamp('expires_at', {
|
||||
withTimezone: true,
|
||||
mode: 'date',
|
||||
|
|
@ -17,6 +17,13 @@ const sessions = pgTable('sessions', {
|
|||
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 { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
import { timestamps } from '../utils';
|
||||
import users from './users';
|
||||
import usersTable from './users.table';
|
||||
|
||||
const twoFactorTable = pgTable('two_factor', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
|
@ -17,15 +17,15 @@ const twoFactorTable = pgTable('two_factor', {
|
|||
}),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id)
|
||||
.references(() => usersTable.id)
|
||||
.unique(),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
|
||||
user: one(users, {
|
||||
user: one(usersTable, {
|
||||
fields: [twoFactorTable.userId],
|
||||
references: [users.id],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
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 users from './users';
|
||||
import usersTable from './users.table';
|
||||
import roles from './roles';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ const user_roles = pgTable('user_roles', {
|
|||
.$defaultFn(() => cuid2()),
|
||||
user_id: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
.references(() => usersTable.id, { onDelete: 'cascade' }),
|
||||
role_id: uuid('role_id')
|
||||
.notNull()
|
||||
.references(() => roles.id, { onDelete: 'cascade' }),
|
||||
|
|
@ -25,9 +25,9 @@ export const user_role_relations = relations(user_roles, ({ one }) => ({
|
|||
fields: [user_roles.role_id],
|
||||
references: [roles.id],
|
||||
}),
|
||||
user: one(users, {
|
||||
user: one(usersTable, {
|
||||
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 user_roles from './userRoles';
|
||||
|
||||
const users = pgTable('users', {
|
||||
const usersTable = pgTable('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
|
|
@ -20,10 +20,10 @@ const users = pgTable('users', {
|
|||
...timestamps,
|
||||
});
|
||||
|
||||
export const user_relations = relations(users, ({ many }) => ({
|
||||
export const userRelations = relations(usersTable, ({ many }) => ({
|
||||
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 { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import users from './users';
|
||||
import usersTable from './users.table';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const wishlists = pgTable('wishlists', {
|
||||
|
|
@ -11,7 +11,7 @@ const wishlists = pgTable('wishlists', {
|
|||
.$defaultFn(() => cuid2()),
|
||||
user_id: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
.references(() => usersTable.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull().default('My Wishlist'),
|
||||
...timestamps,
|
||||
});
|
||||
|
|
@ -19,9 +19,9 @@ const wishlists = pgTable('wishlists', {
|
|||
export type Wishlists = InferSelectModel<typeof wishlists>;
|
||||
|
||||
export const wishlists_relations = relations(wishlists, ({ one }) => ({
|
||||
user: one(users, {
|
||||
user: one(usersTable, {
|
||||
fields: [wishlists.user_id],
|
||||
references: [users.id],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
// import * as Sentry from '@sentry/sveltekit';
|
||||
import { hc } from 'hono/client';
|
||||
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 { 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
|
||||
// Sentry.init({
|
||||
|
|
@ -12,6 +16,39 @@ import { lucia } from '$lib/server/auth';
|
|||
// 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 }) {
|
||||
event.locals.startTimer = Date.now();
|
||||
|
||||
|
|
@ -55,5 +92,6 @@ export const authentication: Handle = async function ({ event, resolve }) {
|
|||
export const handle: Handle = sequence(
|
||||
// Sentry.sentryHandle(),
|
||||
authentication,
|
||||
apiClient
|
||||
);
|
||||
// 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 { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle';
|
||||
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;
|
||||
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