From ea2a9da1e0128e640f82f51938e737acff6cdb64 Mon Sep 17 00:00:00 2001 From: Ryan Pandya Date: Thu, 14 Nov 2024 18:27:22 -0800 Subject: [PATCH] CLI hello-world works and talks to TRPC! And starting to clean things up --- apps/cli/src/commands/hello-world.ts | 3 +- apps/cli/src/lib/trpc.ts | 2 + apps/web/components/settings/AISettings.tsx | 326 -------------- .../components/settings/ChangePassword.tsx | 2 +- apps/web/components/settings/ImportExport.tsx | 217 +-------- .../components/settings/sidebar/Sidebar.tsx | 2 +- .../web/components/settings/sidebar/items.tsx | 54 +-- apps/web/server/api/client.ts | 5 + packages/db/drizzle.ts | 4 +- .../db/migrations/0004_sad_sally_floyd.sql | 4 + .../db/migrations/meta/0004_snapshot.json | 423 ++++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 14 + packages/trpc/auth.ts | 14 + packages/trpc/routers/_app.ts | 5 +- packages/trpc/routers/apiKeys.ts | 114 +++++ 16 files changed, 614 insertions(+), 582 deletions(-) delete mode 100644 apps/web/components/settings/AISettings.tsx create mode 100644 packages/db/migrations/0004_sad_sally_floyd.sql create mode 100644 packages/db/migrations/meta/0004_snapshot.json create mode 100644 packages/trpc/routers/apiKeys.ts diff --git a/apps/cli/src/commands/hello-world.ts b/apps/cli/src/commands/hello-world.ts index b2fddfd..ff7289d 100644 --- a/apps/cli/src/commands/hello-world.ts +++ b/apps/cli/src/commands/hello-world.ts @@ -15,8 +15,9 @@ helloWorldCmd .description("does something specific I guess") .action(async () => { const api = getAPIClient(); + const whoami = await api.users.whoami.query(); try { - console.dir(api); + console.log("Hello " + whoami.name); } catch (error) { printErrorMessageWithReason( "Something went horribly wrong", diff --git a/apps/cli/src/lib/trpc.ts b/apps/cli/src/lib/trpc.ts index f172d73..1d6230a 100644 --- a/apps/cli/src/lib/trpc.ts +++ b/apps/cli/src/lib/trpc.ts @@ -5,9 +5,11 @@ import type { AppRouter } from "@lifetracker/trpc/routers/_app"; export function getAPIClient() { const globals = getGlobalOptions(); + return createTRPCProxyClient({ links: [ httpBatchLink({ + transformer: superjson, url: `${globals.serverAddr}/api/trpc`, maxURLLength: 14000, headers() { diff --git a/apps/web/components/settings/AISettings.tsx b/apps/web/components/settings/AISettings.tsx deleted file mode 100644 index 0a8db14..0000000 --- a/apps/web/components/settings/AISettings.tsx +++ /dev/null @@ -1,326 +0,0 @@ -"use client"; - -import { ActionButton } from "@/components/ui/action-button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/components/ui/form"; -import { FullPageSpinner } from "@/components/ui/full-page-spinner"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { toast } from "@/components/ui/use-toast"; -import { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Plus, Save, Trash2 } from "lucide-react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { buildImagePrompt, buildTextPrompt } from "@hoarder/shared/prompts"; -import { - zNewPromptSchema, - ZPrompt, - zUpdatePromptSchema, -} from "@hoarder/shared/types/prompts"; - -export function PromptEditor() { - const apiUtils = api.useUtils(); - - const form = useForm>({ - resolver: zodResolver(zNewPromptSchema), - defaultValues: { - text: "", - appliesTo: "all", - }, - }); - - const { mutateAsync: createPrompt, isPending: isCreating } = - api.prompts.create.useMutation({ - onSuccess: () => { - toast({ - description: "Prompt has been created!", - }); - apiUtils.prompts.list.invalidate(); - }, - }); - - return ( -
- { - await createPrompt(value); - form.resetField("text"); - })} - > - { - return ( - - - - - - - ); - }} - /> - - { - return ( - - - - - - - ); - }} - /> - - - Add - - - - ); -} - -export function PromptRow({ prompt }: { prompt: ZPrompt }) { - const apiUtils = api.useUtils(); - const { mutateAsync: updatePrompt, isPending: isUpdating } = - api.prompts.update.useMutation({ - onSuccess: () => { - toast({ - description: "Prompt has been updated!", - }); - apiUtils.prompts.list.invalidate(); - }, - }); - const { mutate: deletePrompt, isPending: isDeleting } = - api.prompts.delete.useMutation({ - onSuccess: () => { - toast({ - description: "Prompt has been deleted!", - }); - apiUtils.prompts.list.invalidate(); - }, - }); - - const form = useForm>({ - resolver: zodResolver(zUpdatePromptSchema), - defaultValues: { - promptId: prompt.id, - text: prompt.text, - appliesTo: prompt.appliesTo, - }, - }); - - return ( -
- { - await updatePrompt(value); - })} - > - { - return ( - - - - - - - ); - }} - /> - { - return ( - - - - - - - ); - }} - /> - - { - return ( - - - - - - - ); - }} - /> - - - Save - - deletePrompt({ promptId: prompt.id })} - className="items-center" - type="button" - > - - Delete - - - - ); -} - -export function TaggingRules() { - const { data: prompts, isLoading } = api.prompts.list.useQuery(); - - return ( -
-
Tagging Rules
-

- Prompts that you add here will be included as rules to the model during - tag generation. You can view the final prompts in the prompt preview - section. -

- {isLoading && } - {prompts && prompts.length == 0 && ( -

- You don't have any custom prompts yet. -

- )} - {prompts && - prompts.map((prompt) => )} - -
- ); -} - -export function PromptDemo() { - const { data: prompts } = api.prompts.list.useQuery(); - const clientConfig = useClientConfig(); - return ( -
-
- Prompt Preview -
-

Text Prompt

- - {buildTextPrompt( - clientConfig.inference.inferredTagLang, - (prompts ?? []) - .filter((p) => p.appliesTo == "text" || p.appliesTo == "all") - .map((p) => p.text), - "\n\n", - /* context length */ 1024 /* The value here doesn't matter */, - ).trim()} - -

Image Prompt

- - {buildImagePrompt( - clientConfig.inference.inferredTagLang, - (prompts ?? []) - .filter((p) => p.appliesTo == "images" || p.appliesTo == "all") - .map((p) => p.text), - ).trim()} - -
- ); -} - -export default function AISettings() { - return ( - <> -
-
-
- AI Settings -
- -
-
-
- -
- - ); -} diff --git a/apps/web/components/settings/ChangePassword.tsx b/apps/web/components/settings/ChangePassword.tsx index aa27f22..eb71e8a 100644 --- a/apps/web/components/settings/ChangePassword.tsx +++ b/apps/web/components/settings/ChangePassword.tsx @@ -16,7 +16,7 @@ import { api } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; -import { zChangePasswordSchema } from "@hoarder/shared/types/users"; +import { zChangePasswordSchema } from "@lifetracker/shared/types/users"; export function ChangePassword() { const form = useForm>({ diff --git a/apps/web/components/settings/ImportExport.tsx b/apps/web/components/settings/ImportExport.tsx index 7889b4d..548d5d5 100644 --- a/apps/web/components/settings/ImportExport.tsx +++ b/apps/web/components/settings/ImportExport.tsx @@ -48,223 +48,8 @@ export function ExportButton() { export function ImportExportRow() { const router = useRouter(); - const [importProgress, setImportProgress] = useState<{ - done: number; - total: number; - } | null>(null); - - const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook(); - const { mutateAsync: updateBookmark } = useUpdateBookmark(); - const { mutateAsync: createList } = useCreateBookmarkList(); - const { mutateAsync: addToList } = useAddBookmarkToList(); - const { mutateAsync: updateTags } = useUpdateBookmarkTags(); - - const { mutateAsync: parseAndCreateBookmark } = useMutation({ - mutationFn: async (toImport: { - bookmark: ParsedBookmark; - listId: string; - }) => { - const bookmark = toImport.bookmark; - if (bookmark.content === undefined) { - throw new Error("Content is undefined"); - } - const created = await createBookmark( - bookmark.content.type === BookmarkTypes.LINK - ? { - type: BookmarkTypes.LINK, - url: bookmark.content.url, - } - : { - type: BookmarkTypes.TEXT, - text: bookmark.content.text, - }, - ); - - await Promise.all([ - // Update title and createdAt if they're set - bookmark.title.length > 0 || bookmark.addDate - ? updateBookmark({ - bookmarkId: created.id, - title: bookmark.title, - createdAt: bookmark.addDate - ? new Date(bookmark.addDate * 1000) - : undefined, - note: bookmark.notes, - }).catch(() => { - /* empty */ - }) - : undefined, - - // Add to import list - addToList({ - bookmarkId: created.id, - listId: toImport.listId, - }).catch((e) => { - if ( - e instanceof TRPCClientError && - e.message.includes("already in the list") - ) { - /* empty */ - } else { - throw e; - } - }), - - // Update tags - bookmark.tags.length > 0 - ? updateTags({ - bookmarkId: created.id, - attach: bookmark.tags.map((t) => ({ tagName: t })), - detach: [], - }) - : undefined, - ]); - return created; - }, - }); - - const { mutateAsync: runUploadBookmarkFile } = useMutation({ - mutationFn: async ({ - file, - source, - }: { - file: File; - source: "html" | "pocket" | "omnivore" | "hoarder"; - }) => { - if (source === "html") { - return await parseNetscapeBookmarkFile(file); - } else if (source === "pocket") { - return await parsePocketBookmarkFile(file); - } else if (source === "hoarder") { - return await parseHoarderBookmarkFile(file); - } else if (source === "omnivore") { - return await parseOmnivoreBookmarkFile(file); - } else { - throw new Error("Unknown source"); - } - }, - onSuccess: async (resp) => { - const importList = await createList({ - name: `Imported Bookmarks`, - icon: "⬆️", - }); - setImportProgress({ done: 0, total: resp.length }); - - const successes = []; - const failed = []; - const alreadyExisted = []; - // Do the imports one by one - for (const parsedBookmark of resp) { - try { - const result = await parseAndCreateBookmark({ - bookmark: parsedBookmark, - listId: importList.id, - }); - if (result.alreadyExists) { - alreadyExisted.push(parsedBookmark); - } else { - successes.push(parsedBookmark); - } - } catch (e) { - failed.push(parsedBookmark); - } - setImportProgress((prev) => ({ - done: (prev?.done ?? 0) + 1, - total: resp.length, - })); - } - - if (successes.length > 0 || alreadyExisted.length > 0) { - toast({ - description: `Imported ${successes.length} bookmarks and skipped ${alreadyExisted.length} bookmarks that already existed`, - variant: "default", - }); - } - - if (failed.length > 0) { - toast({ - description: `Failed to import ${failed.length} bookmarks`, - variant: "destructive", - }); - } - - router.push(`/dashboard/lists/${importList.id}`); - }, - onError: (error) => { - toast({ - description: error.message, - variant: "destructive", - }); - }, - }); - return ( -
-
- - runUploadBookmarkFile({ file, source: "html" }) - } - > - -

Import Bookmarks from HTML file

-
- - - runUploadBookmarkFile({ file, source: "pocket" }) - } - > - -

Import Bookmarks from Pocket export

-
- - runUploadBookmarkFile({ file, source: "omnivore" }) - } - > - -

Import Bookmarks from Omnivore export

-
- - runUploadBookmarkFile({ file, source: "hoarder" }) - } - > - -

Import Bookmarks from Hoarder export

-
- -
- {importProgress && ( -
-

- Processed {importProgress.done} of {importProgress.total} bookmarks -

-
- -
-
- )} -
+
TBD!
); } diff --git a/apps/web/components/settings/sidebar/Sidebar.tsx b/apps/web/components/settings/sidebar/Sidebar.tsx index 247e091..10150e2 100644 --- a/apps/web/components/settings/sidebar/Sidebar.tsx +++ b/apps/web/components/settings/sidebar/Sidebar.tsx @@ -2,7 +2,7 @@ import { redirect } from "next/navigation"; import SidebarItem from "@/components/shared/sidebar/SidebarItem"; import { getServerAuthSession } from "@/server/auth"; -import serverConfig from "@hoarder/shared/config"; +import serverConfig from "@lifetracker/shared/config"; import { settingsSidebarItems } from "./items"; diff --git a/apps/web/components/settings/sidebar/items.tsx b/apps/web/components/settings/sidebar/items.tsx index 047ee23..8665d80 100644 --- a/apps/web/components/settings/sidebar/items.tsx +++ b/apps/web/components/settings/sidebar/items.tsx @@ -3,8 +3,6 @@ import { ArrowLeft, Download, KeyRound, - Rss, - Sparkles, User, } from "lucide-react"; @@ -13,34 +11,24 @@ export const settingsSidebarItems: { icon: JSX.Element; path: string; }[] = [ - { - name: "Back To App", - icon: , - path: "/dashboard/bookmarks", - }, - { - name: "User Info", - icon: , - path: "/settings/info", - }, - { - name: "AI Settings", - icon: , - path: "/settings/ai", - }, - { - name: "RSS Subscriptions", - icon: , - path: "/settings/feeds", - }, - { - name: "Import / Export", - icon: , - path: "/settings/import", - }, - { - name: "API Keys", - icon: , - path: "/settings/api-keys", - }, -]; + { + name: "Back To App", + icon: , + path: "/dashboard/today", + }, + { + name: "User Info", + icon: , + path: "/settings/info", + }, + { + name: "Import / Export", + icon: , + path: "/settings/import", + }, + { + name: "API Keys", + icon: , + path: "/settings/api-keys", + }, + ]; diff --git a/apps/web/server/api/client.ts b/apps/web/server/api/client.ts index e1205e2..0532e64 100644 --- a/apps/web/server/api/client.ts +++ b/apps/web/server/api/client.ts @@ -17,6 +17,11 @@ export async function createContextFromRequest(req: Request) { if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) { const token = authorizationHeader.split(" ")[1]; try { + + console.log("\n\nEeeeeeentering hell\n\n"); + console.log(await authenticateApiKey(token)); + console.log("\n\n\n\n"); + const user = await authenticateApiKey(token); return { user, diff --git a/packages/db/drizzle.ts b/packages/db/drizzle.ts index 81ad8e6..8c9fc28 100644 --- a/packages/db/drizzle.ts +++ b/packages/db/drizzle.ts @@ -8,9 +8,7 @@ import path from "path"; import dbConfig from "./drizzle.config"; const sqlite = new Database(dbConfig.dbCredentials.url); - -console.log(dbConfig.dbCredentials.url); -export const db = drizzle(sqlite, { schema }); +export const db = drizzle(sqlite, { schema, logger: true }); export function getInMemoryDB(runMigrations: boolean) { const mem = new Database(":memory:"); diff --git a/packages/db/migrations/0004_sad_sally_floyd.sql b/packages/db/migrations/0004_sad_sally_floyd.sql new file mode 100644 index 0000000..25978b1 --- /dev/null +++ b/packages/db/migrations/0004_sad_sally_floyd.sql @@ -0,0 +1,4 @@ +CREATE TABLE `config` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL +); diff --git a/packages/db/migrations/meta/0004_snapshot.json b/packages/db/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..4941b63 --- /dev/null +++ b/packages/db/migrations/meta/0004_snapshot.json @@ -0,0 +1,423 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "bcdf4680-869f-4b4a-9690-1bd45a196387", + "prevId": "67edcacc-ad95-4e34-b368-4beeda535072", + "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": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "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": {} + }, + "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": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "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 459f516..21f43b3 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1731542202120, "tag": "0003_complex_ares", "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1731637559763, + "tag": "0004_sad_sally_floyd", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/schema.ts b/packages/db/schema.ts index ddabdcd..2571cf5 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -104,3 +104,17 @@ export const days = sqliteTable("day", { date: text("date").notNull().unique(), comment: text("comment").notNull(), }); + +export const config = sqliteTable("config", { + key: text("key").notNull().primaryKey(), + value: text("value").notNull(), +}); + +// Relations + +export const apiKeyRelations = relations(apiKeys, ({ one }) => ({ + user: one(users, { + fields: [apiKeys.userId], + references: [users.id], + }), +})); diff --git a/packages/trpc/auth.ts b/packages/trpc/auth.ts index 4e968a6..97b637b 100644 --- a/packages/trpc/auth.ts +++ b/packages/trpc/auth.ts @@ -54,7 +54,21 @@ function parseApiKey(plain: string) { } export async function authenticateApiKey(key: string) { + const { keyId, keySecret } = parseApiKey(key); + + console.log("\n\nWELCOME TO HELL\n\n"); + console.log(parseApiKey(key)); + // Console.log all rows in the apiKeys table + console.log(await db.query.apiKeys.findMany()); + console.log(await db.query.apiKeys.findFirst({ + where: (k, { eq }) => eq(k.keyId, keyId), + with: { + user: true, + }, + })); + console.log("\n\n\n\n"); + const apiKey = await db.query.apiKeys.findFirst({ where: (k, { eq }) => eq(k.keyId, keyId), with: { diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts index 9a3c8fb..f1397dd 100644 --- a/packages/trpc/routers/_app.ts +++ b/packages/trpc/routers/_app.ts @@ -1,8 +1,11 @@ +import { apiKeys } from "@lifetracker/db/schema"; import { router } from "../index"; import { usersAppRouter } from "./users"; +import { apiKeysAppRouter } from "./apiKeys"; export const appRouter = router({ - users: usersAppRouter + users: usersAppRouter, + apiKeys: apiKeysAppRouter, }); // export type definition of API export type AppRouter = typeof appRouter; \ No newline at end of file diff --git a/packages/trpc/routers/apiKeys.ts b/packages/trpc/routers/apiKeys.ts new file mode 100644 index 0000000..14aecbe --- /dev/null +++ b/packages/trpc/routers/apiKeys.ts @@ -0,0 +1,114 @@ +import { TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; + +import { apiKeys } from "@lifetracker/db/schema"; +import serverConfig from "@lifetracker/shared/config"; + +import { + authenticateApiKey, + generateApiKey, + logAuthenticationError, + validatePassword, +} from "../auth"; +import { authedProcedure, publicProcedure, router } from "../index"; + +const zApiKeySchema = z.object({ + id: z.string(), + name: z.string(), + key: z.string(), + createdAt: z.date(), +}); + +export const apiKeysAppRouter = router({ + create: authedProcedure + .input( + z.object({ + name: z.string(), + }), + ) + .output(zApiKeySchema) + .mutation(async ({ input, ctx }) => { + return await generateApiKey(input.name, ctx.user.id); + }), + revoke: authedProcedure + .input( + z.object({ + id: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + await ctx.db + .delete(apiKeys) + .where(and(eq(apiKeys.id, input.id), eq(apiKeys.userId, ctx.user.id))); + }), + list: authedProcedure + .output( + z.object({ + keys: z.array( + z.object({ + id: z.string(), + name: z.string(), + createdAt: z.date(), + keyId: z.string(), + }), + ), + }), + ) + .query(async ({ ctx }) => { + const resp = await ctx.db.query.apiKeys.findMany({ + where: eq(apiKeys.userId, ctx.user.id), + columns: { + id: true, + name: true, + createdAt: true, + keyId: true, + }, + }); + return { keys: resp }; + }), + // Exchange the username and password with an API key. + // Homemade oAuth. This is used by the extension. + exchange: publicProcedure + .input( + z.object({ + keyName: z.string(), + email: z.string(), + password: z.string(), + }), + ) + .output(zApiKeySchema) + .mutation(async ({ input, ctx }) => { + let user; + // Special handling as otherwise the extension would show "username or password is wrong" + if (serverConfig.auth.disablePasswordAuth) { + throw new TRPCError({ + message: "Password authentication is currently disabled", + code: "FORBIDDEN", + }); + } + try { + user = await validatePassword(input.email, input.password); + } catch (e) { + const error = e as Error; + logAuthenticationError(input.email, error.message, ctx.req.ip); + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return await generateApiKey(input.keyName, user.id); + }), + validate: publicProcedure + .input(z.object({ apiKey: z.string() })) + .output(z.object({ success: z.boolean() })) + .mutation(async ({ input, ctx }) => { + try { + await authenticateApiKey(input.apiKey); // Throws if the key is invalid + return { + success: true, + }; + } catch (e) { + const error = e as Error; + logAuthenticationError("", error.message, ctx.req.ip); + throw e; + } + }), +}); \ No newline at end of file