From c023fa1730c01373e961d7eac7386a232df55632 Mon Sep 17 00:00:00 2001 From: Ryan Pandya Date: Wed, 13 Nov 2024 16:23:53 -0800 Subject: [PATCH] Added auth, refactored schema, almost have TRPC MVP working --- apps/web/app/api/health/route.ts | 8 + apps/web/app/api/trpc/route.ts | 22 + apps/web/app/server/api/client.ts | 60 +++ apps/web/package.json | 22 +- apps/web/server/api/client.ts | 60 +++ apps/web/server/auth.ts | 191 +++++++ apps/web/tsconfig.json | 15 +- packages/db/drizzle.config.ts | 2 +- packages/db/drizzle.ts | 5 +- packages/db/lifetracker.db | Bin 24576 -> 77824 bytes packages/db/migrations/0002_minor_toxin.sql | 53 ++ packages/db/migrations/0003_complex_ares.sql | 3 + .../db/migrations/meta/0002_snapshot.json | 400 +++++++++++++++ .../db/migrations/meta/0003_snapshot.json | 402 +++++++++++++++ packages/db/migrations/meta/_journal.json | 14 + packages/db/package.json | 2 + packages/db/schema.ts | 106 ++++ packages/db/schema/auth.ts | 99 ++++ packages/db/schema/day.ts | 2 +- packages/shared/README.md | 1 + packages/shared/config.ts | 150 ++++++ packages/shared/logger.ts | 36 ++ packages/shared/package.json | 27 + packages/shared/tsconfig.json | 13 + packages/trpc/auth.ts | 115 +++++ packages/trpc/index.ts | 18 + packages/trpc/package.json | 1 + pnpm-lock.yaml | 470 ++++++++++++++++-- 28 files changed, 2252 insertions(+), 45 deletions(-) create mode 100644 apps/web/app/api/health/route.ts create mode 100644 apps/web/app/api/trpc/route.ts create mode 100644 apps/web/app/server/api/client.ts create mode 100644 apps/web/server/api/client.ts create mode 100644 apps/web/server/auth.ts create mode 100644 packages/db/migrations/0002_minor_toxin.sql create mode 100644 packages/db/migrations/0003_complex_ares.sql create mode 100644 packages/db/migrations/meta/0002_snapshot.json create mode 100644 packages/db/migrations/meta/0003_snapshot.json create mode 100644 packages/db/schema.ts create mode 100644 packages/db/schema/auth.ts create mode 100644 packages/shared/README.md create mode 100644 packages/shared/config.ts create mode 100644 packages/shared/logger.ts create mode 100644 packages/shared/package.json create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/trpc/auth.ts diff --git a/apps/web/app/api/health/route.ts b/apps/web/app/api/health/route.ts new file mode 100644 index 0000000..7f0fd5c --- /dev/null +++ b/apps/web/app/api/health/route.ts @@ -0,0 +1,8 @@ +import { NextRequest, NextResponse } from "next/server"; + +export const GET = async (_req: NextRequest) => { + return NextResponse.json({ + status: "ok", + message: "Web app is working", + }); +}; diff --git a/apps/web/app/api/trpc/route.ts b/apps/web/app/api/trpc/route.ts new file mode 100644 index 0000000..94abda4 --- /dev/null +++ b/apps/web/app/api/trpc/route.ts @@ -0,0 +1,22 @@ +import { createContextFromRequest } from "@/server/api/client"; +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; + +import { appRouter } from "@lifetracker/trpc/routers/_app"; + +const handler = (req: Request) => + fetchRequestHandler({ + endpoint: "/api/trpc", + req, + router: appRouter, + onError: ({ path, error }) => { + if (process.env.NODE_ENV === "development") { + console.error(`❌ tRPC failed on ${path}`); + } + console.error(error); + }, + + createContext: async (opts) => { + return await createContextFromRequest(opts.req); + }, + }); +export { handler as GET, handler as POST }; diff --git a/apps/web/app/server/api/client.ts b/apps/web/app/server/api/client.ts new file mode 100644 index 0000000..80d4274 --- /dev/null +++ b/apps/web/app/server/api/client.ts @@ -0,0 +1,60 @@ +import { headers } from "next/headers"; +import { getServerAuthSession } from "@/server/auth"; +import requestIp from "request-ip"; + +import { db } from "@lifetracker/db"; +import { Context, createCallerFactory } from "@lifetracker/trpc"; +import { authenticateApiKey } from "@lifetracker/trpc/auth"; +import { appRouter } from "@lifetracker/trpc/routers/_app"; + +export async function createContextFromRequest(req: Request) { + // TODO: This is a hack until we offer a proper REST API instead of the trpc based one. + // Check if the request has an Authorization token, if it does, assume that API key authentication is requested. + const ip = requestIp.getClientIp({ + headers: Object.fromEntries(req.headers.entries()), + }); + const authorizationHeader = req.headers.get("Authorization"); + if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) { + const token = authorizationHeader.split(" ")[1]; + try { + const user = await authenticateApiKey(token); + return { + user, + db, + req: { + ip, + }, + }; + } catch (e) { + // Fallthrough to cookie-based auth + } + } + + return createContext(db, ip); +} + +export const createContext = async ( + database?: typeof db, + ip?: string | null, +): Promise => { + const session = await getServerAuthSession(); + if (ip === undefined) { + const hdrs = headers(); + ip = requestIp.getClientIp({ + headers: Object.fromEntries(hdrs.entries()), + }); + } + return { + user: session?.user ?? null, + db: database ?? db, + req: { + ip, + }, + }; +}; + +const createCaller = createCallerFactory(appRouter); + +export const api = createCaller(createContext); + +export const createTrcpClientFromCtx = createCaller; \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 000c605..c279cb8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,17 +3,29 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbo", + "dev": "next dev", + "clean": "git clean -xdf .next .turbo node_modules", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test": "vitest", + "typecheck": "tsc --noEmit", + "format": "prettier --check . --ignore-path ../../.gitignore" }, "dependencies": { + "@auth/drizzle-adapter": "^1.7.3", "@lifetracker/db": "workspace:^", + "@lifetracker/shared": "workspace:^", + "@lifetracker/trpc": "workspace:^", "@lifetracker/ui": "workspace:*", + "@trpc/client": "^10.45.2", + "@trpc/server": "^10.45.2", + "drizzle-orm": "^0.33.0", "next": "14.2.6", + "next-auth": "^4.24.10", "react": "18.3.1", - "react-dom": "18.3.1" + "react-dom": "18.3.1", + "request-ip": "^3.3.0" }, "devDependencies": { "@lifetracker/eslint-config": "workspace:*", @@ -21,8 +33,10 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/request-ip": "^0.0.41", "eslint": "^8", "eslint-config-next": "14.2.6", - "typescript": "^5" + "typescript": "^5", + "vite-tsconfig-paths": "^4.3.1" } } \ No newline at end of file diff --git a/apps/web/server/api/client.ts b/apps/web/server/api/client.ts new file mode 100644 index 0000000..80d4274 --- /dev/null +++ b/apps/web/server/api/client.ts @@ -0,0 +1,60 @@ +import { headers } from "next/headers"; +import { getServerAuthSession } from "@/server/auth"; +import requestIp from "request-ip"; + +import { db } from "@lifetracker/db"; +import { Context, createCallerFactory } from "@lifetracker/trpc"; +import { authenticateApiKey } from "@lifetracker/trpc/auth"; +import { appRouter } from "@lifetracker/trpc/routers/_app"; + +export async function createContextFromRequest(req: Request) { + // TODO: This is a hack until we offer a proper REST API instead of the trpc based one. + // Check if the request has an Authorization token, if it does, assume that API key authentication is requested. + const ip = requestIp.getClientIp({ + headers: Object.fromEntries(req.headers.entries()), + }); + const authorizationHeader = req.headers.get("Authorization"); + if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) { + const token = authorizationHeader.split(" ")[1]; + try { + const user = await authenticateApiKey(token); + return { + user, + db, + req: { + ip, + }, + }; + } catch (e) { + // Fallthrough to cookie-based auth + } + } + + return createContext(db, ip); +} + +export const createContext = async ( + database?: typeof db, + ip?: string | null, +): Promise => { + const session = await getServerAuthSession(); + if (ip === undefined) { + const hdrs = headers(); + ip = requestIp.getClientIp({ + headers: Object.fromEntries(hdrs.entries()), + }); + } + return { + user: session?.user ?? null, + db: database ?? db, + req: { + ip, + }, + }; +}; + +const createCaller = createCallerFactory(appRouter); + +export const api = createCaller(createContext); + +export const createTrcpClientFromCtx = createCaller; \ No newline at end of file diff --git a/apps/web/server/auth.ts b/apps/web/server/auth.ts new file mode 100644 index 0000000..3df7133 --- /dev/null +++ b/apps/web/server/auth.ts @@ -0,0 +1,191 @@ +import type { Adapter } from "next-auth/adapters"; +import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import { and, count, eq } from "drizzle-orm"; +import NextAuth, { + DefaultSession, + getServerSession, + NextAuthOptions, +} from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { Provider } from "next-auth/providers/index"; +import requestIp from "request-ip"; + +import { db } from "@lifetracker/db"; +import { + accounts, + sessions, + users, + verificationTokens, +} from "@lifetracker/db/schema"; +import serverConfig from "@lifetracker/shared/config"; +import { logAuthenticationError, validatePassword } from "@lifetracker/trpc/auth"; + +type UserRole = "admin" | "user"; + +declare module "next-auth/jwt" { + export interface JWT { + user: { + id: string; + role: UserRole; + } & DefaultSession["user"]; + } +} + +declare module "next-auth" { + /** + * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context + */ + export interface Session { + user: { + id: string; + role: UserRole; + } & DefaultSession["user"]; + } + + export interface DefaultUser { + role: UserRole | null; + } +} + +/** + * Returns true if the user table is empty, which indicates that this user is going to be + * the first one. This can be racy if multiple users are created at the same time, but + * that should be fine. + */ +async function isFirstUser(): Promise { + const [{ count: userCount }] = await db + .select({ count: count() }) + .from(users); + return userCount == 0; +} + +/** + * Returns true if the user is an admin + */ +async function isAdmin(email: string): Promise { + const res = await db.query.users.findFirst({ + columns: { role: true }, + where: eq(users.email, email), + }); + return res?.role == "admin"; +} + +const providers: Provider[] = [ + CredentialsProvider({ + // The name to display on the sign in form (e.g. "Sign in with...") + name: "Credentials", + credentials: { + email: { label: "Email", type: "email", placeholder: "Email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials, req) { + if (!credentials) { + return null; + } + + try { + return await validatePassword( + credentials?.email, + credentials?.password, + ); + } catch (e) { + const error = e as Error; + logAuthenticationError( + credentials?.email, + error.message, + requestIp.getClientIp({ headers: req.headers }), + ); + return null; + } + }, + }), +]; + +const oauth = serverConfig.auth.oauth; +if (oauth.wellKnownUrl) { + providers.push({ + id: "custom", + name: oauth.name, + type: "oauth", + wellKnown: oauth.wellKnownUrl, + authorization: { params: { scope: oauth.scope } }, + clientId: oauth.clientId, + clientSecret: oauth.clientSecret, + allowDangerousEmailAccountLinking: oauth.allowDangerousEmailAccountLinking, + idToken: true, + checks: ["pkce", "state"], + async profile(profile: Record) { + const [admin, firstUser] = await Promise.all([ + isAdmin(profile.email), + isFirstUser(), + ]); + return { + id: profile.sub, + name: profile.name || profile.email, + email: profile.email, + image: profile.picture, + role: admin || firstUser ? "admin" : "user", + }; + }, + }); +} + +export const authOptions: NextAuthOptions = { + // https://github.com/nextauthjs/next-auth/issues/9493 + adapter: DrizzleAdapter(db, { + usersTable: users, + accountsTable: accounts, + sessionsTable: sessions, + verificationTokensTable: verificationTokens, + }) as Adapter, + providers: providers, + session: { + strategy: "jwt", + }, + pages: { + signIn: "/signin", + signOut: "/signin", + error: "/signin", + newUser: "/signin", + }, + callbacks: { + async signIn({ credentials, profile }) { + if (credentials) { + return true; + } + if (!profile?.email) { + throw new Error("No profile"); + } + const [{ count: userCount }] = await db + .select({ count: count() }) + .from(users) + .where(and(eq(users.email, profile.email))); + + // If it's a new user and signups are disabled, fail the sign in + if (userCount === 0 && serverConfig.auth.disableSignups) { + throw new Error("Signups are disabled in server config"); + } + return true; + }, + async jwt({ token, user }) { + if (user) { + token.user = { + id: user.id, + name: user.name, + email: user.email, + image: user.image, + role: user.role ?? "user", + }; + } + return token; + }, + async session({ session, token }) { + session.user = { ...token.user }; + return session; + }, + }, +}; + +export const authHandler = NextAuth(authOptions); + +export const getServerAuthSession = () => getServerSession(authOptions); \ No newline at end of file diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index fc04bd9..b147ea1 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,20 +1,27 @@ { - "extends": "@lifetracker/typescript-config/nextjs.json", + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@lifetracker/typescript-config/base.json", "compilerOptions": { + "baseUrl": ".", "plugins": [ { "name": "next" } - ] + ], + "paths": { + "@/*": [ + "./*" + ] + } }, "include": [ "next-env.d.ts", - "next.config.mjs", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts" ], "exclude": [ - "node_modules" + "node_modules", + ".next" ] } \ No newline at end of file diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts index 8c86597..ca9ada7 100644 --- a/packages/db/drizzle.config.ts +++ b/packages/db/drizzle.config.ts @@ -5,7 +5,7 @@ const databaseURL = "./lifetracker.db"; export default { dialect: "sqlite", - schema: "./schema", + schema: "./schema.ts", out: "./migrations", dbCredentials: { url: databaseURL, diff --git a/packages/db/drizzle.ts b/packages/db/drizzle.ts index 27abc26..d3e74c2 100644 --- a/packages/db/drizzle.ts +++ b/packages/db/drizzle.ts @@ -1,20 +1,21 @@ import "dotenv/config"; import { drizzle } from "drizzle-orm/better-sqlite3"; import Database from "better-sqlite3"; -import * as schema from "./schema/day"; +import * as schema from "./schema"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import path from "path"; import dbConfig from "./drizzle.config"; const sqlite = new Database(dbConfig.dbCredentials.url); + export const db = drizzle(sqlite, { schema }); export function getInMemoryDB(runMigrations: boolean) { const mem = new Database(":memory:"); const db = drizzle(mem, { schema, logger: true }); if (runMigrations) { - migrate(db, { migrationsFolder: path.resolve(__dirname, "./drizzle") }); + migrate(db, { migrationsFolder: path.resolve(__dirname, "./migrations") }); } return db; } diff --git a/packages/db/lifetracker.db b/packages/db/lifetracker.db index 54e5cb09bc0da2d2d235582e87dc7f519808528d..fb3e8562f16ea2314948e2303d6c3e408f18678a 100644 GIT binary patch literal 77824 zcmeI&&2t*(9l&wcNFY!o9BUj#bq_nb$oQpR#M{yyS{mVm5y7@Bwee&U*q3K*CIS>d zj+GoT*qvTFy)-kux3`{p==8tne~^Emms~o1JM_@!*LSkA8u-+ZNGZ!xV6~nOb+YM?#p#7c-+W!8lZRhy&fnL{jB4vr@SgUv zGPLX?@m%!VUfZ#T?Oykw_p#_+DoBM59#no@uj;K!`{huQiB{Vc-Jz^W^jrE+ycp_@ z2M2oNuwK6-cMes>Gs?w_<91&RT6()X6i-Ee@}!6Rwf8FfPxQU&lbe&Z-_gVBGRaIO z6-yu9(;~ju;FBYH(QjHO!=C@Sd8wz(?4_csqsMQ=)7P(SuM$3oin4mHO%GZXjmH^| zW1@eP-43F9nIOooIpsg`gCN{92!+<}SWiXJnEH5Z4F;d~`tlgXt4jL4qj0~zTfJX7 ztRLvxs-xS<%=TI={e$cCVAMg)ta@D^9VO%G8#lC9_x#}x#9$y3A$(h%MhHs-hwy`+ zGXFtDH6#;!#@JsI{`(L1tF?C;D*SS)gjHnp{p$Vdezoy-^>H|LRc^F0YAPQ-+*Px_ z+tV#aWwR>Vt=6lu%&`WJ<%(qH?rJPuxiQZr98)tJZoKjG)=E6RxvBju&Y2>LrC9p*ZS6_000tM4fso%Nx-!PoAD5?kePQQKbv?f$&&;I$8M*bj9Q3F1 zDK9OfN8^DHV_|#K-()Tv7hLCM zp`0MW7&G-`W|Yz5=|Vvpo%oG6n{L1T*=I+h*=axR`&awHtgrLIo|N+z9rJd^9ubUcFcaD|f-MDLz-hiBfo5=(zjn74Aw>O(U-t28QI zSCs9Vd@im3DU$j_%1dp^4IT&}fB*srAb3OS=- z7>?)JWi#t|PQfgdglFY#FDJ6ulH(Rig?wIQ%jKeJ?Fi2nrGn{NMn3OoUvB@cw6YiV zE3*rB*)0gyHuI+GSh<{S3bSl@rk&doL?aQ^_e6hQy`IR{$>j=j&vUypj;TW#r+VW_pRP~qAjVHsCu~)jYbmc{=fXEe;x=RfB*srAbgl zE#(kxje%ITMY6Jq>xwegXXd4(R)93erzDo7#+T-07M6nTV4M7(Rg?v6>_a|L#^TN2 t`1lw(nE2Be_&4*XZ5C9B;-8!*KMyGKoq_)k|M$&;2Cw-yf7j statement-breakpoint +CREATE TABLE `apiKey` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `createdAt` integer NOT NULL, + `keyId` text NOT NULL, + `keyHash` text NOT NULL, + `userId` text NOT NULL, + FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `session` ( + `sessionToken` text PRIMARY KEY NOT NULL, + `userId` text NOT NULL, + `expires` integer NOT NULL, + FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `user` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL, + `emailVerified` integer, + `image` text, + `password` text, + `role` text DEFAULT 'user' +); +--> statement-breakpoint +CREATE TABLE `verificationToken` ( + `identifier` text NOT NULL, + `token` text NOT NULL, + `expires` integer NOT NULL, + PRIMARY KEY(`identifier`, `token`) +); +--> statement-breakpoint +CREATE UNIQUE INDEX `apiKey_keyId_unique` ON `apiKey` (`keyId`);--> statement-breakpoint +CREATE UNIQUE INDEX `apiKey_name_userId_unique` ON `apiKey` (`name`,`userId`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); \ No newline at end of file diff --git a/packages/db/migrations/0003_complex_ares.sql b/packages/db/migrations/0003_complex_ares.sql new file mode 100644 index 0000000..27f10a4 --- /dev/null +++ b/packages/db/migrations/0003_complex_ares.sql @@ -0,0 +1,3 @@ +ALTER TABLE `days` RENAME TO `day`;--> statement-breakpoint +DROP INDEX IF EXISTS `days_date_unique`;--> statement-breakpoint +CREATE UNIQUE INDEX `day_date_unique` ON `day` (`date`); \ No newline at end of file diff --git a/packages/db/migrations/meta/0002_snapshot.json b/packages/db/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..2f6ea24 --- /dev/null +++ b/packages/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,400 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "709a800b-1a51-4e31-96ef-0e8dd1c42a95", + "prevId": "6ad45ca2-1f2d-4e94-a8fd-b749a8577a61", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {} + }, + "days": { + "name": "days", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "days_date_unique": { + "name": "days_date_unique", + "columns": [ + "date" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/0003_snapshot.json b/packages/db/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..c4c058c --- /dev/null +++ b/packages/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,402 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "67edcacc-ad95-4e34-b368-4beeda535072", + "prevId": "709a800b-1a51-4e31-96ef-0e8dd1c42a95", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {} + }, + "day": { + "name": "day", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "day_date_unique": { + "name": "day_date_unique", + "columns": [ + "date" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": { + "\"days\"": "\"day\"" + }, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index ce081cf..459f516 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -15,6 +15,20 @@ "when": 1731100507972, "tag": "0001_hard_lionheart", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1731542160896, + "tag": "0002_minor_toxin", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1731542202120, + "tag": "0003_complex_ares", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/package.json b/packages/db/package.json index d526368..f0e19a3 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -12,6 +12,8 @@ "studio": "drizzle-kit studio" }, "dependencies": { + "@auth/core": "^0.37.3", + "@paralleldrive/cuid2": "^2.2.2", "better-sqlite3": "^9.4.3", "dotenv": "^16.4.1", "drizzle-orm": "^0.33.0", diff --git a/packages/db/schema.ts b/packages/db/schema.ts new file mode 100644 index 0000000..ddabdcd --- /dev/null +++ b/packages/db/schema.ts @@ -0,0 +1,106 @@ +import type { AdapterAccount } from "@auth/core/adapters"; +import { createId } from "@paralleldrive/cuid2"; +import { relations } from "drizzle-orm"; +import { + AnySQLiteColumn, + index, + integer, + primaryKey, + sqliteTable, + text, + unique, +} from "drizzle-orm/sqlite-core"; + +function createdAtField() { + return integer("createdAt", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()); +} + +export const apiKeys = sqliteTable( + "apiKey", + { + id: text("id") + .notNull() + .primaryKey() + .$defaultFn(() => createId()), + name: text("name").notNull(), + createdAt: createdAtField(), + keyId: text("keyId").notNull().unique(), + keyHash: text("keyHash").notNull(), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + }, + (ak) => ({ + unq: unique().on(ak.name, ak.userId), + }), +); + + +export const users = sqliteTable("user", { + id: text("id") + .notNull() + .primaryKey() + .$defaultFn(() => createId()), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: integer("emailVerified", { mode: "timestamp_ms" }), + image: text("image"), + password: text("password"), + role: text("role", { enum: ["admin", "user"] }).default("user"), +}); + +export const accounts = sqliteTable( + "account", + { + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + type: text("type").$type().notNull(), + provider: text("provider").notNull(), + providerAccountId: text("providerAccountId").notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: integer("expires_at"), + token_type: text("token_type"), + scope: text("scope"), + id_token: text("id_token"), + session_state: text("session_state"), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + }), +); + +export const sessions = sqliteTable("session", { + sessionToken: text("sessionToken") + .notNull() + .primaryKey() + .$defaultFn(() => createId()), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + expires: integer("expires", { mode: "timestamp_ms" }).notNull(), +}); + +export const verificationTokens = sqliteTable( + "verificationToken", + { + identifier: text("identifier").notNull(), + token: text("token").notNull(), + expires: integer("expires", { mode: "timestamp_ms" }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }), +); + +export const days = sqliteTable("day", { + id: integer("id").primaryKey({ autoIncrement: true }), + mood: integer("mood"), + date: text("date").notNull().unique(), + comment: text("comment").notNull(), +}); diff --git a/packages/db/schema/auth.ts b/packages/db/schema/auth.ts new file mode 100644 index 0000000..2a4aea4 --- /dev/null +++ b/packages/db/schema/auth.ts @@ -0,0 +1,99 @@ +import type { AdapterAccount } from "@auth/core/adapters"; +import { createId } from "@paralleldrive/cuid2"; +import { relations } from "drizzle-orm"; +import { + AnySQLiteColumn, + index, + integer, + primaryKey, + sqliteTable, + text, + unique, +} from "drizzle-orm/sqlite-core"; + +function createdAtField() { + return integer("createdAt", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()); +} + +export const apiKeys = sqliteTable( + "apiKey", + { + id: text("id") + .notNull() + .primaryKey() + .$defaultFn(() => createId()), + name: text("name").notNull(), + createdAt: createdAtField(), + keyId: text("keyId").notNull().unique(), + keyHash: text("keyHash").notNull(), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + }, + (ak) => ({ + unq: unique().on(ak.name, ak.userId), + }), +); + + +export const users = sqliteTable("user", { + id: text("id") + .notNull() + .primaryKey() + .$defaultFn(() => createId()), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: integer("emailVerified", { mode: "timestamp_ms" }), + image: text("image"), + password: text("password"), + role: text("role", { enum: ["admin", "user"] }).default("user"), +}); + +export const accounts = sqliteTable( + "account", + { + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + type: text("type").$type().notNull(), + provider: text("provider").notNull(), + providerAccountId: text("providerAccountId").notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: integer("expires_at"), + token_type: text("token_type"), + scope: text("scope"), + id_token: text("id_token"), + session_state: text("session_state"), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + }), +); + +export const sessions = sqliteTable("session", { + sessionToken: text("sessionToken") + .notNull() + .primaryKey() + .$defaultFn(() => createId()), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + expires: integer("expires", { mode: "timestamp_ms" }).notNull(), +}); + +export const verificationTokens = sqliteTable( + "verificationToken", + { + identifier: text("identifier").notNull(), + token: text("token").notNull(), + expires: integer("expires", { mode: "timestamp_ms" }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }), +); diff --git a/packages/db/schema/day.ts b/packages/db/schema/day.ts index 13358be..ba55ea1 100644 --- a/packages/db/schema/day.ts +++ b/packages/db/schema/day.ts @@ -1,6 +1,6 @@ import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core"; -export const days = sqliteTable("days", { +export const days = sqliteTable("day", { id: integer("id").primaryKey({ autoIncrement: true }), mood: integer("mood"), date: text("date").notNull().unique(), diff --git a/packages/shared/README.md b/packages/shared/README.md new file mode 100644 index 0000000..3ad6681 --- /dev/null +++ b/packages/shared/README.md @@ -0,0 +1 @@ +# `@lifetracker/shared` \ No newline at end of file diff --git a/packages/shared/config.ts b/packages/shared/config.ts new file mode 100644 index 0000000..b372dca --- /dev/null +++ b/packages/shared/config.ts @@ -0,0 +1,150 @@ +import { z } from "zod"; + +const stringBool = (defaultValue: string) => + z + .string() + .default(defaultValue) + .refine((s) => s === "true" || s === "false") + .transform((s) => s === "true"); + +const allEnv = z.object({ + API_URL: z.string().url().default("http://localhost:3000"), + DISABLE_SIGNUPS: stringBool("false"), + DISABLE_PASSWORD_AUTH: stringBool("false"), + OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING: stringBool("false"), + OAUTH_WELLKNOWN_URL: z.string().url().optional(), + OAUTH_CLIENT_SECRET: z.string().optional(), + OAUTH_CLIENT_ID: z.string().optional(), + OAUTH_SCOPE: z.string().default("openid email profile"), + OAUTH_PROVIDER_NAME: z.string().default("Custom Provider"), + OPENAI_API_KEY: z.string().optional(), + OPENAI_BASE_URL: z.string().url().optional(), + OLLAMA_BASE_URL: z.string().url().optional(), + OLLAMA_KEEP_ALIVE: z.string().optional(), + INFERENCE_JOB_TIMEOUT_SEC: z.coerce.number().default(30), + INFERENCE_TEXT_MODEL: z.string().default("gpt-4o-mini"), + INFERENCE_IMAGE_MODEL: z.string().default("gpt-4o-mini"), + INFERENCE_CONTEXT_LENGTH: z.coerce.number().default(2048), + OCR_CACHE_DIR: z.string().optional(), + OCR_LANGS: z + .string() + .default("eng") + .transform((val) => val.split(",")), + OCR_CONFIDENCE_THRESHOLD: z.coerce.number().default(50), + CRAWLER_HEADLESS_BROWSER: stringBool("true"), + BROWSER_WEB_URL: z.string().url().optional(), + BROWSER_WEBSOCKET_URL: z.string().url().optional(), + BROWSER_CONNECT_ONDEMAND: stringBool("false"), + CRAWLER_JOB_TIMEOUT_SEC: z.coerce.number().default(60), + CRAWLER_NAVIGATE_TIMEOUT_SEC: z.coerce.number().default(30), + CRAWLER_NUM_WORKERS: z.coerce.number().default(1), + CRAWLER_DOWNLOAD_BANNER_IMAGE: stringBool("true"), + CRAWLER_STORE_SCREENSHOT: stringBool("true"), + CRAWLER_FULL_PAGE_SCREENSHOT: stringBool("false"), + CRAWLER_FULL_PAGE_ARCHIVE: stringBool("false"), + CRAWLER_VIDEO_DOWNLOAD: stringBool("false"), + CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE: z.coerce.number().default(50), + CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC: z.coerce.number().default(10 * 60), + MEILI_ADDR: z.string().optional(), + MEILI_MASTER_KEY: z.string().default(""), + LOG_LEVEL: z.string().default("debug"), + DEMO_MODE: stringBool("false"), + DEMO_MODE_EMAIL: z.string().optional(), + DEMO_MODE_PASSWORD: z.string().optional(), + DATA_DIR: z.string().default(""), + MAX_ASSET_SIZE_MB: z.coerce.number().default(4), + INFERENCE_LANG: z.string().default("english"), + // Build only flag + SERVER_VERSION: z.string().optional(), + DISABLE_NEW_RELEASE_CHECK: stringBool("false"), + + // A flag to detect if the user is running in the old separete containers setup + USING_LEGACY_SEPARATE_CONTAINERS: stringBool("false"), +}); + +const serverConfigSchema = allEnv.transform((val) => { + return { + apiUrl: val.API_URL, + auth: { + disableSignups: val.DISABLE_SIGNUPS, + disablePasswordAuth: val.DISABLE_PASSWORD_AUTH, + oauth: { + allowDangerousEmailAccountLinking: + val.OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING, + wellKnownUrl: val.OAUTH_WELLKNOWN_URL, + clientSecret: val.OAUTH_CLIENT_SECRET, + clientId: val.OAUTH_CLIENT_ID, + scope: val.OAUTH_SCOPE, + name: val.OAUTH_PROVIDER_NAME, + }, + }, + inference: { + jobTimeoutSec: val.INFERENCE_JOB_TIMEOUT_SEC, + openAIApiKey: val.OPENAI_API_KEY, + openAIBaseUrl: val.OPENAI_BASE_URL, + ollamaBaseUrl: val.OLLAMA_BASE_URL, + ollamaKeepAlive: val.OLLAMA_KEEP_ALIVE, + textModel: val.INFERENCE_TEXT_MODEL, + imageModel: val.INFERENCE_IMAGE_MODEL, + inferredTagLang: val.INFERENCE_LANG, + contextLength: val.INFERENCE_CONTEXT_LENGTH, + }, + crawler: { + numWorkers: val.CRAWLER_NUM_WORKERS, + headlessBrowser: val.CRAWLER_HEADLESS_BROWSER, + browserWebUrl: val.BROWSER_WEB_URL, + browserWebSocketUrl: val.BROWSER_WEBSOCKET_URL, + browserConnectOnDemand: val.BROWSER_CONNECT_ONDEMAND, + jobTimeoutSec: val.CRAWLER_JOB_TIMEOUT_SEC, + navigateTimeoutSec: val.CRAWLER_NAVIGATE_TIMEOUT_SEC, + downloadBannerImage: val.CRAWLER_DOWNLOAD_BANNER_IMAGE, + storeScreenshot: val.CRAWLER_STORE_SCREENSHOT, + fullPageScreenshot: val.CRAWLER_FULL_PAGE_SCREENSHOT, + fullPageArchive: val.CRAWLER_FULL_PAGE_ARCHIVE, + downloadVideo: val.CRAWLER_VIDEO_DOWNLOAD, + maxVideoDownloadSize: val.CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE, + downloadVideoTimeout: val.CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC, + }, + ocr: { + langs: val.OCR_LANGS, + cacheDir: val.OCR_CACHE_DIR, + confidenceThreshold: val.OCR_CONFIDENCE_THRESHOLD, + }, + meilisearch: val.MEILI_ADDR + ? { + address: val.MEILI_ADDR, + key: val.MEILI_MASTER_KEY, + } + : undefined, + logLevel: val.LOG_LEVEL, + demoMode: val.DEMO_MODE + ? { + email: val.DEMO_MODE_EMAIL, + password: val.DEMO_MODE_PASSWORD, + } + : undefined, + dataDir: val.DATA_DIR, + maxAssetSizeMb: val.MAX_ASSET_SIZE_MB, + serverVersion: val.SERVER_VERSION, + disableNewReleaseCheck: val.DISABLE_NEW_RELEASE_CHECK, + usingLegacySeparateContainers: val.USING_LEGACY_SEPARATE_CONTAINERS, + }; +}); + +const serverConfig = serverConfigSchema.parse(process.env); +// Always explicitly pick up stuff from server config to avoid accidentally leaking stuff +export const clientConfig = { + demoMode: serverConfig.demoMode, + auth: { + disableSignups: serverConfig.auth.disableSignups, + disablePasswordAuth: serverConfig.auth.disablePasswordAuth, + }, + inference: { + inferredTagLang: serverConfig.inference.inferredTagLang, + }, + serverVersion: serverConfig.serverVersion, + disableNewReleaseCheck: serverConfig.disableNewReleaseCheck, +}; +export type ClientConfig = typeof clientConfig; + +export default serverConfig; \ No newline at end of file diff --git a/packages/shared/logger.ts b/packages/shared/logger.ts new file mode 100644 index 0000000..b6c5c28 --- /dev/null +++ b/packages/shared/logger.ts @@ -0,0 +1,36 @@ +import winston from "winston"; + +import serverConfig from "./config"; + +const logger = winston.createLogger({ + level: serverConfig.logLevel, + format: winston.format.combine( + winston.format.timestamp(), + winston.format.colorize(), + winston.format.printf( + (info) => `${info.timestamp} ${info.level}: ${info.message}`, + ), + ), + transports: [new winston.transports.Console()], +}); + +export default logger; + +export const authFailureLogger = winston.createLogger({ + level: "debug", + format: winston.format.combine( + winston.format.timestamp(), + winston.format.printf( + (info) => `${info.timestamp} ${info.level}: ${info.message}`, + ), + ), + transports: [ + new winston.transports.Console(), + new winston.transports.File({ + filename: "auth_failures.log", + dirname: serverConfig.dataDir, + maxFiles: 2, + maxsize: 1024 * 1024, + }), + ], +}); \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..890b360 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@lifetracker/shared", + "version": "0.1.0", + "private": true, + "type": "module", + "dependencies": { + "winston": "^3.17.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@lifetracker/eslint-config": "workspace:^", + "@lifetracker/typescript-config": "workspace:^" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "format": "prettier . --ignore-path ../../.prettierignore", + "lint": "eslint ." + }, + "main": "index.ts", + "eslintConfig": { + "root": true, + "extends": [ + "@lifetracker/eslint-config/base" + ] + } +} \ No newline at end of file diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..51884a1 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@lifetracker/typescript-config/node.json", + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules" + ], + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, +} \ No newline at end of file diff --git a/packages/trpc/auth.ts b/packages/trpc/auth.ts new file mode 100644 index 0000000..14374f8 --- /dev/null +++ b/packages/trpc/auth.ts @@ -0,0 +1,115 @@ +import { randomBytes } from "crypto"; +import * as bcrypt from "bcryptjs"; + +import { db } from "@lifetracker/db"; +import { apiKeys } from "@lifetracker/db/schema/auth"; +import serverConfig from "@lifetracker/shared/config"; +import { authFailureLogger } from "@lifetracker/shared/logger"; + +// API Keys + +const BCRYPT_SALT_ROUNDS = 10; +const API_KEY_PREFIX = "ak1"; + +export async function generateApiKey(name: string, userId: string) { + const id = randomBytes(10).toString("hex"); + const secret = randomBytes(10).toString("hex"); + const secretHash = await bcrypt.hash(secret, BCRYPT_SALT_ROUNDS); + + const plain = `${API_KEY_PREFIX}_${id}_${secret}`; + + const key = ( + await db + .insert(apiKeys) + .values({ + name: name, + userId: userId, + keyId: id, + keyHash: secretHash, + }) + .returning() + )[0]; + + return { + id: key.id, + name: key.name, + createdAt: key.createdAt, + key: plain, + }; +} +function parseApiKey(plain: string) { + const parts = plain.split("_"); + if (parts.length != 3) { + throw new Error( + `Malformed API key. API keys should have 3 segments, found ${parts.length} instead.`, + ); + } + if (parts[0] !== API_KEY_PREFIX) { + throw new Error(`Malformed API key. Got unexpected key prefix.`); + } + return { + keyId: parts[1], + keySecret: parts[2], + }; +} + +export async function authenticateApiKey(key: string) { + const { keyId, keySecret } = parseApiKey(key); + const apiKey = await db.query.apiKeys.findFirst({ + where: (k, { eq }) => eq(k.keyId, keyId), + with: { + user: true, + }, + }); + + if (!apiKey) { + throw new Error("API key not found"); + } + + const hash = apiKey.keyHash; + + const validation = await bcrypt.compare(keySecret, hash); + if (!validation) { + throw new Error("Invalid API Key"); + } + + return apiKey.user; +} + +export async function hashPassword(password: string) { + return bcrypt.hash(password, BCRYPT_SALT_ROUNDS); +} + +export async function validatePassword(email: string, password: string) { + if (serverConfig.auth.disablePasswordAuth) { + throw new Error("Password authentication is currently disabled"); + } + const user = await db.query.users.findFirst({ + where: (u, { eq }) => eq(u.email, email), + }); + + if (!user) { + throw new Error("User not found"); + } + + if (!user.password) { + throw new Error("This user doesn't have a password defined"); + } + + const validation = await bcrypt.compare(password, user.password); + if (!validation) { + throw new Error("Wrong password"); + } + + return user; +} + +export function logAuthenticationError( + user: string, + message: string, + ip: string | null, +): void { + authFailureLogger.error( + `Authentication error. User: "${user}", Message: "${message}", IP-Address: "${ip}"`, + ); +} \ No newline at end of file diff --git a/packages/trpc/index.ts b/packages/trpc/index.ts index 7055f2b..8a087fc 100644 --- a/packages/trpc/index.ts +++ b/packages/trpc/index.ts @@ -1,5 +1,23 @@ import { initTRPC } from "@trpc/server"; +import type { db } from "@lifetracker/db"; const t = initTRPC.create(); export const router = t.router; export const publicProcedure = t.procedure; + +export const createCallerFactory = t.createCallerFactory; + + +interface User { + id: string; + name?: string | null | undefined; + email?: string | null | undefined; + role: "admin" | "user" | null; +} +export interface Context { + user: User | null; + db: typeof db; + req: { + ip: string | null; + }; +} \ No newline at end of file diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 292f0b1..09899d4 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@lifetracker/db": "workspace:*", + "@lifetracker/shared": "workspace:^", "@lifetracker/ui": "workspace:*", "@trpc/server": "^10.45.2", "bcryptjs": "^2.4.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 819e991..34d9f32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,21 +127,45 @@ importers: apps/web: dependencies: + '@auth/drizzle-adapter': + specifier: ^1.7.3 + version: 1.7.3 '@lifetracker/db': specifier: workspace:^ version: link:../../packages/db + '@lifetracker/shared': + specifier: workspace:^ + version: link:../../packages/shared + '@lifetracker/trpc': + specifier: workspace:^ + version: link:../../packages/trpc '@lifetracker/ui': specifier: workspace:* version: link:../../packages/ui + '@trpc/client': + specifier: ^10.45.2 + version: 10.45.2(@trpc/server@10.45.2) + '@trpc/server': + specifier: ^10.45.2 + version: 10.45.2 + drizzle-orm: + specifier: ^0.33.0 + version: 0.33.0(@types/better-sqlite3@7.6.11)(@types/react@18.2.61)(better-sqlite3@9.6.0)(expo-sqlite@14.0.6(expo@51.0.36(@babel/core@7.26.0)(@babel/preset-env@7.25.7(@babel/core@7.26.0))))(react@18.3.1) next: specifier: 14.2.6 - version: 14.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.6(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-auth: + specifier: ^4.24.10 + version: 4.24.10(next@14.2.6(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) + request-ip: + specifier: ^3.3.0 + version: 3.3.0 devDependencies: '@lifetracker/eslint-config': specifier: workspace:* @@ -158,6 +182,9 @@ importers: '@types/react-dom': specifier: ^18 version: 18.2.19 + '@types/request-ip': + specifier: ^0.0.41 + version: 0.0.41 eslint: specifier: ^8 version: 8.57.0 @@ -167,9 +194,18 @@ importers: typescript: specifier: ^5 version: 5.3.3 + vite-tsconfig-paths: + specifier: ^4.3.1 + version: 4.3.2(typescript@5.3.3)(vite@5.4.10(@types/node@20.11.24)(terser@5.34.1)) packages/db: dependencies: + '@auth/core': + specifier: ^0.37.3 + version: 0.37.3 + '@paralleldrive/cuid2': + specifier: ^2.2.2 + version: 2.2.2 better-sqlite3: specifier: ^9.4.3 version: 9.6.0 @@ -226,11 +262,30 @@ importers: specifier: ^5.3.3 version: 5.3.3 + packages/shared: + dependencies: + winston: + specifier: ^3.17.0 + version: 3.17.0 + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@lifetracker/eslint-config': + specifier: workspace:^ + version: link:../eslint-config + '@lifetracker/typescript-config': + specifier: workspace:^ + version: link:../typescript-config + packages/trpc: dependencies: '@lifetracker/db': specifier: workspace:* version: link:../db + '@lifetracker/shared': + specifier: workspace:^ + version: link:../shared '@lifetracker/ui': specifier: workspace:* version: link:../ui @@ -391,6 +446,23 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@auth/core@0.37.3': + resolution: {integrity: sha512-qcffDLwxB9iUYH8GHq68w/KU8jtjAbjjk9xnpoKhjX3+QcntaQ2MKVSkTTocmA6ElpL5vK2xR9CXfQ98dvGnyg==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^6.8.0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + + '@auth/drizzle-adapter@1.7.3': + resolution: {integrity: sha512-0BuGgRjcghoFJQH8796BaLCFXqh77lhizXuDliVK7cF2hs1nXo7eKZ/m5ddFa7ATTOwQ3gncQLHFJ1ap8dOoew==} + '@babel/code-frame@7.10.4': resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} @@ -1526,6 +1598,10 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + '@commander-js/extra-typings@12.1.0': resolution: {integrity: sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg==} peerDependencies: @@ -1535,6 +1611,9 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -1720,9 +1799,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -2528,6 +2609,10 @@ packages: '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} + '@noble/hashes@1.5.0': + resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2544,6 +2629,12 @@ packages: resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2993,6 +3084,9 @@ packages: '@types/react@18.2.61': resolution: {integrity: sha512-NURTN0qNnJa7O/k4XUkEW2yfygA+NxS0V5h1+kp9jPwhzZy95q3ADoGMP0+JypMhrZBTTgjKAUlTctde1zzeQA==} + '@types/request-ip@0.0.41': + resolution: {integrity: sha512-Qzz0PM2nSZej4lsLzzNfADIORZhhxO7PED0fXpg4FjXiHuJ/lMyUg+YFF5q8x9HPZH3Gl6N+NOM8QZjItNgGKg==} + '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} @@ -3023,6 +3117,9 @@ packages: '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -3505,6 +3602,9 @@ packages: async-limiter@1.0.1: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynciterator.prototype@1.0.0: resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==} @@ -3943,12 +4043,21 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + combine-promises@1.2.0: resolution: {integrity: sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==} engines: {node: '>=10'} @@ -4053,6 +4162,14 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.0.1: + resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==} + engines: {node: '>=18'} + copy-anything@3.0.5: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} @@ -4606,6 +4723,9 @@ packages: emoticon@4.1.0: resolution: {integrity: sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -4896,6 +5016,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -5088,6 +5209,9 @@ packages: fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + feed@4.2.2: resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} engines: {node: '>=0.4.0'} @@ -5174,6 +5298,9 @@ packages: resolution: {integrity: sha512-z8hKPUjZ33VLn4HVntifqmEhmolUMopysnMNzazoDqo1GLUkBsreLNsxETlKJMPotUWStQnen6SGvUNe1j4Hlg==} engines: {node: '>=0.4.0'} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -5797,6 +5924,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-async-function@2.0.0: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} engines: {node: '>= 0.4'} @@ -6102,6 +6232,12 @@ packages: join-component@1.1.0: resolution: {integrity: sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==} + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + + jose@5.9.6: + resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -6185,6 +6321,9 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} @@ -6327,6 +6466,10 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -6350,10 +6493,6 @@ packages: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lru-cache@10.2.0: - resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} - engines: {node: 14 || >=16.14} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -6782,6 +6921,20 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} + next-auth@4.24.10: + resolution: {integrity: sha512-8NGqiRO1GXBcVfV8tbbGcUgQkAGsX4GRzzXXea4lDikAsJtD5KiEY34bfhUOjHLvr6rT6afpcxw2H8EZqOV6aQ==} + peerDependencies: + '@auth/core': 0.34.2 + next: ^12.2.5 || ^13 || ^14 || ^15 + nodemailer: ^6.6.5 + react: ^17.0.2 || ^18 + react-dom: ^17.0.2 || ^18 + peerDependenciesMeta: + '@auth/core': + optional: true + nodemailer: + optional: true + next@14.2.6: resolution: {integrity: sha512-57Su7RqXs5CBKKKOagt8gPhMM3CpjgbeQhrtei2KLAA1vTNm7jfKS+uDARkSW8ZETUflDCBIsUKGSyQdRs4U4g==} engines: {node: '>=18.17.0'} @@ -6886,10 +7039,20 @@ packages: nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + oauth4webapi@3.1.2: + resolution: {integrity: sha512-KQZkNU+xn02lWrFu5Vjqg9E81yPtDSxUZorRHlLWVoojD+H/0GFbH59kcnz5Thdjj7c4/mYMBPj/mhvGe/kKXA==} + + oauth@0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -6930,6 +7093,10 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + oidc-token-hash@5.0.3: + resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} + engines: {node: ^10.13.0 || >=12.0.0} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -6945,6 +7112,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + onetime@2.0.1: resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} engines: {node: '>=4'} @@ -6973,6 +7143,9 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true + openid-client@5.7.0: + resolution: {integrity: sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==} + optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -7473,6 +7646,19 @@ packages: pouchdb-collections@1.0.1: resolution: {integrity: sha512-31db6JRg4+4D5Yzc2nqsRqsA2oOkZS8DpFav3jf/qVNBxusKa2ClkEIZ2bJNpaDbMfWtnuSq59p6Bn+CipPMdg==} + preact-render-to-string@5.2.6: + resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} + peerDependencies: + preact: '>=10' + + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} + peerDependencies: + preact: '>=10' + + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + prebuild-install@7.1.2: resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} engines: {node: '>=10'} @@ -7506,6 +7692,9 @@ packages: resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} engines: {node: '>= 6'} + pretty-format@3.8.0: + resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + pretty-time@1.1.0: resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} engines: {node: '>=4'} @@ -7818,6 +8007,9 @@ packages: renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + request-ip@3.3.0: + resolution: {integrity: sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -7949,6 +8141,10 @@ packages: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -8113,6 +8309,9 @@ packages: simple-plist@1.3.1: resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -8232,6 +8431,9 @@ packages: resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -8489,6 +8691,9 @@ packages: engines: {node: '>=10'} hasBin: true + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -8579,6 +8784,10 @@ packages: resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} engines: {node: '>=0.10.0'} + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -8963,6 +9172,14 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-tsconfig-paths@4.3.2: + resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite-tsconfig-paths@5.1.0: resolution: {integrity: sha512-Y1PLGHCJfAq1Zf4YIGEsmuU/NCX1epoZx9zwSr32Gjn3aalwQHRKr5aUmbo6r0JHeHkqmWpmDg7WOynhYXw1og==} peerDependencies: @@ -9149,6 +9366,14 @@ packages: wildcard@2.0.1: resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + engines: {node: '>= 12.0.0'} + wonka@4.0.15: resolution: {integrity: sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg==} @@ -9393,6 +9618,23 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@auth/core@0.37.3': + dependencies: + '@panva/hkdf': 1.2.1 + cookie: 1.0.1 + jose: 5.9.6 + oauth4webapi: 3.1.2 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + + '@auth/drizzle-adapter@1.7.3': + dependencies: + '@auth/core': 0.37.3 + transitivePeerDependencies: + - '@simplewebauthn/browser' + - '@simplewebauthn/server' + - nodemailer + '@babel/code-frame@7.10.4': dependencies: '@babel/highlight': 7.25.9 @@ -9427,7 +9669,7 @@ snapshots: '@babel/traverse': 7.23.3 '@babel/types': 7.23.3 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -11559,6 +11801,8 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@colors/colors@1.6.0': {} + '@commander-js/extra-typings@12.1.0(commander@12.1.0)': dependencies: commander: 12.1.0 @@ -11567,6 +11811,12 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + '@discoveryjs/json-ext@0.5.7': {} '@docsearch/css@3.6.2': {} @@ -12543,7 +12793,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.7 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -12854,7 +13104,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.4 + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -13017,6 +13267,8 @@ snapshots: dependencies: eslint-scope: 5.1.1 + '@noble/hashes@1.5.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -13033,6 +13285,12 @@ snapshots: dependencies: semver: 7.6.3 + '@panva/hkdf@1.2.1': {} + + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.5.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -13582,6 +13840,10 @@ snapshots: '@types/scheduler': 0.16.3 csstype: 3.1.2 + '@types/request-ip@0.0.41': + dependencies: + '@types/node': 20.11.24 + '@types/retry@0.12.0': {} '@types/sax@1.2.7': @@ -13617,6 +13879,8 @@ snapshots: '@types/tinycolor2@1.4.6': {} + '@types/triple-beam@1.3.5': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -13643,7 +13907,7 @@ snapshots: '@typescript-eslint/type-utils': 6.17.0(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.17.0(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.17.0 - debug: 4.3.4 + debug: 4.3.7 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 @@ -13681,7 +13945,7 @@ snapshots: '@typescript-eslint/types': 6.17.0 '@typescript-eslint/typescript-estree': 6.17.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.17.0 - debug: 4.3.4 + debug: 4.3.7 eslint: 8.57.0 optionalDependencies: typescript: 5.3.3 @@ -13732,7 +13996,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3) '@typescript-eslint/utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3) - debug: 4.3.4 + debug: 4.3.7 eslint: 8.57.0 ts-api-utils: 1.0.2(typescript@5.3.3) optionalDependencies: @@ -13779,7 +14043,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.1.0 '@typescript-eslint/visitor-keys': 7.1.0 - debug: 4.3.4 + debug: 4.3.7 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -14246,6 +14510,8 @@ snapshots: async-limiter@1.0.1: {} + async@3.2.6: {} + asynciterator.prototype@1.0.0: dependencies: has-symbols: 1.0.3 @@ -14808,10 +15074,25 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + colord@2.9.3: {} colorette@2.0.20: {} + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + combine-promises@1.2.0: {} combined-stream@1.0.8: @@ -14905,6 +15186,10 @@ snapshots: cookie@0.6.0: {} + cookie@0.7.2: {} + + cookie@1.0.1: {} + copy-anything@3.0.5: dependencies: is-what: 4.1.16 @@ -15180,7 +15465,7 @@ snapshots: debug@3.2.7: dependencies: - ms: 2.1.2 + ms: 2.1.3 debug@4.3.4: dependencies: @@ -15437,6 +15722,8 @@ snapshots: emoticon@4.1.0: {} + enabled@2.0.0: {} + encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -15611,7 +15898,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.19.12): dependencies: - debug: 4.3.4 + debug: 4.3.7 esbuild: 0.19.12 transitivePeerDependencies: - supports-color @@ -15750,7 +16037,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-react: 7.33.2(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) @@ -15783,7 +16070,7 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.17.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.0)(eslint@8.57.0): dependencies: - debug: 4.3.4 + debug: 4.3.7 enhanced-resolve: 5.15.0 eslint: 8.57.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.17.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.17.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.0)(eslint@8.57.0))(eslint@8.57.0) @@ -15800,11 +16087,11 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0): dependencies: - debug: 4.3.4 + debug: 4.3.7 enhanced-resolve: 5.15.0 eslint: 8.57.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.1 get-tsconfig: 4.7.2 is-core-module: 2.13.1 @@ -15843,7 +16130,7 @@ snapshots: eslint: 8.57.0 ignore: 5.3.1 - eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 @@ -16342,6 +16629,8 @@ snapshots: transitivePeerDependencies: - encoding + fecha@4.2.3: {} + feed@4.2.2: dependencies: xml-js: 1.6.11 @@ -16443,6 +16732,8 @@ snapshots: flow-parser@0.252.0: {} + fn.name@1.1.0: {} + follow-redirects@1.15.9: {} fontfaceobserver@2.3.0: {} @@ -16933,7 +17224,7 @@ snapshots: history@4.10.1: dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.26.0 loose-envify: 1.4.0 resolve-pathname: 3.0.0 tiny-invariant: 1.3.3 @@ -17217,6 +17508,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-async-function@2.0.0: dependencies: has-tostringtag: 1.0.0 @@ -17485,6 +17778,10 @@ snapshots: join-component@1.1.0: {} + jose@4.15.9: {} + + jose@5.9.6: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -17581,6 +17878,8 @@ snapshots: kleur@3.0.3: {} + kuler@2.0.0: {} + language-subtag-registry@0.3.22: {} language-tags@1.0.9: @@ -17703,6 +18002,15 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -17723,8 +18031,6 @@ snapshots: lowercase-keys@3.0.0: {} - lru-cache@10.2.0: {} - lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -18405,7 +18711,22 @@ snapshots: netmask@2.0.2: {} - next@14.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-auth@4.24.10(next@14.2.6(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@panva/hkdf': 1.2.1 + cookie: 0.7.2 + jose: 4.15.9 + next: 14.2.6(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + oauth: 0.9.15 + openid-client: 5.7.0 + preact: 10.24.3 + preact-render-to-string: 5.2.6(preact@10.24.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + uuid: 8.3.2 + + next@14.2.6(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.6 '@swc/helpers': 0.5.5 @@ -18415,7 +18736,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.26.0)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.6 '@next/swc-darwin-x64': 14.2.6 @@ -18522,8 +18843,14 @@ snapshots: nullthrows@1.1.1: {} + oauth4webapi@3.1.2: {} + + oauth@0.9.15: {} + object-assign@4.1.1: {} + object-hash@2.2.0: {} + object-inspect@1.13.1: {} object-inspect@1.13.2: {} @@ -18576,6 +18903,8 @@ snapshots: obuf@1.1.2: {} + oidc-token-hash@5.0.3: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -18590,6 +18919,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@2.0.1: dependencies: mimic-fn: 1.2.0 @@ -18622,6 +18955,13 @@ snapshots: opener@1.5.2: {} + openid-client@5.7.0: + dependencies: + jose: 4.15.9 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.0.3 + optionator@0.9.3: dependencies: '@aashutoshrathi/word-wrap': 1.2.6 @@ -18834,7 +19174,7 @@ snapshots: path-scurry@1.10.1: dependencies: - lru-cache: 10.2.0 + lru-cache: 10.4.3 minipass: 7.0.4 path-scurry@1.11.1: @@ -19271,6 +19611,17 @@ snapshots: pouchdb-collections@1.0.1: {} + preact-render-to-string@5.2.6(preact@10.24.3): + dependencies: + preact: 10.24.3 + pretty-format: 3.8.0 + + preact-render-to-string@6.5.11(preact@10.24.3): + dependencies: + preact: 10.24.3 + + preact@10.24.3: {} + prebuild-install@7.1.2: dependencies: detect-libc: 2.0.3 @@ -19311,6 +19662,8 @@ snapshots: ansi-styles: 3.2.1 react-is: 16.13.1 + pretty-format@3.8.0: {} + pretty-time@1.1.0: {} prism-react-renderer@2.4.0(react@18.3.1): @@ -19352,7 +19705,7 @@ snapshots: proxy-agent@6.3.0: dependencies: agent-base: 7.1.0 - debug: 4.3.4 + debug: 4.3.7 http-proxy-agent: 7.0.0 https-proxy-agent: 7.0.1 lru-cache: 7.18.3 @@ -19461,7 +19814,7 @@ snapshots: react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.26.0 invariant: 2.2.4 prop-types: 15.8.1 react: 18.3.1 @@ -19484,7 +19837,7 @@ snapshots: react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.95.0): dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.26.0 react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' webpack: 5.95.0 @@ -19492,13 +19845,13 @@ snapshots: react-router-config@5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.26.0 react: 18.3.1 react-router: 5.3.4(react@18.3.1) react-router-dom@5.3.4(react@18.3.1): dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.26.0 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -19509,7 +19862,7 @@ snapshots: react-router@5.3.4(react@18.3.1): dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.26.0 history: 4.10.1 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -19599,7 +19952,7 @@ snapshots: regenerator-transform@0.15.2: dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.26.0 regexp-tree@0.1.27: {} @@ -19737,6 +20090,8 @@ snapshots: lodash: 4.17.21 strip-ansi: 6.0.1 + request-ip@3.3.0: {} + require-from-string@2.0.2: {} require-like@0.1.2: {} @@ -19889,6 +20244,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.1.4 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sax@1.4.1: {} @@ -20115,6 +20472,10 @@ snapshots: bplist-parser: 0.3.1 plist: 3.1.0 + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.28 @@ -20248,6 +20609,8 @@ snapshots: dependencies: minipass: 7.1.2 + stack-trace@0.0.10: {} + stackback@0.0.2: {} stacktrace-parser@0.1.10: @@ -20384,10 +20747,12 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.1(react@18.3.1): + styled-jsx@5.1.1(@babel/core@7.26.0)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 + optionalDependencies: + '@babel/core': 7.26.0 stylehacks@6.1.1(postcss@8.4.31): dependencies: @@ -20531,6 +20896,8 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + text-hex@1.0.0: {} + text-table@0.2.0: {} thenify-all@1.6.0: @@ -20601,6 +20968,8 @@ snapshots: trim-right@1.0.1: {} + triple-beam@1.4.1: {} + trough@2.2.0: {} ts-api-utils@1.0.2(typescript@5.3.3): @@ -20627,6 +20996,10 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tsconfck@3.1.4(typescript@5.3.3): + optionalDependencies: + typescript: 5.3.3 + tsconfck@3.1.4(typescript@5.6.3): optionalDependencies: typescript: 5.6.3 @@ -21010,6 +21383,17 @@ snapshots: - supports-color - terser + vite-tsconfig-paths@4.3.2(typescript@5.3.3)(vite@5.4.10(@types/node@20.11.24)(terser@5.34.1)): + dependencies: + debug: 4.3.7 + globrex: 0.1.2 + tsconfck: 3.1.4(typescript@5.3.3) + optionalDependencies: + vite: 5.4.10(@types/node@20.11.24)(terser@5.34.1) + transitivePeerDependencies: + - supports-color + - typescript + vite-tsconfig-paths@5.1.0(typescript@5.6.3)(vite@5.4.10(@types/node@20.11.24)(terser@5.34.1)): dependencies: debug: 4.3.7 @@ -21282,6 +21666,26 @@ snapshots: wildcard@2.0.1: {} + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.17.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + wonka@4.0.15: {} wordwrap@1.0.0: {}