CLI hello-world works and talks to TRPC! And starting to clean things up
This commit is contained in:
parent
0fd8ce7189
commit
ea2a9da1e0
@ -15,8 +15,9 @@ helloWorldCmd
|
|||||||
.description("does something specific I guess")
|
.description("does something specific I guess")
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
const api = getAPIClient();
|
const api = getAPIClient();
|
||||||
|
const whoami = await api.users.whoami.query();
|
||||||
try {
|
try {
|
||||||
console.dir(api);
|
console.log("Hello " + whoami.name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
printErrorMessageWithReason(
|
printErrorMessageWithReason(
|
||||||
"Something went horribly wrong",
|
"Something went horribly wrong",
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import type { AppRouter } from "@lifetracker/trpc/routers/_app";
|
|||||||
|
|
||||||
export function getAPIClient() {
|
export function getAPIClient() {
|
||||||
const globals = getGlobalOptions();
|
const globals = getGlobalOptions();
|
||||||
|
|
||||||
return createTRPCProxyClient<AppRouter>({
|
return createTRPCProxyClient<AppRouter>({
|
||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
httpBatchLink({
|
||||||
|
transformer: superjson,
|
||||||
url: `${globals.serverAddr}/api/trpc`,
|
url: `${globals.serverAddr}/api/trpc`,
|
||||||
maxURLLength: 14000,
|
maxURLLength: 14000,
|
||||||
headers() {
|
headers() {
|
||||||
|
|||||||
@ -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<z.infer<typeof zNewPromptSchema>>({
|
|
||||||
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 (
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
className="flex gap-2"
|
|
||||||
onSubmit={form.handleSubmit(async (value) => {
|
|
||||||
await createPrompt(value);
|
|
||||||
form.resetField("text");
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="text"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Add a custom prompt"
|
|
||||||
type="text"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="appliesTo"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem className="flex-0">
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Applies To" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value="all">All</SelectItem>
|
|
||||||
<SelectItem value="text">Text</SelectItem>
|
|
||||||
<SelectItem value="images">Images</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ActionButton
|
|
||||||
type="submit"
|
|
||||||
loading={isCreating}
|
|
||||||
variant="default"
|
|
||||||
className="items-center"
|
|
||||||
>
|
|
||||||
<Plus className="mr-2 size-4" />
|
|
||||||
Add
|
|
||||||
</ActionButton>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<z.infer<typeof zUpdatePromptSchema>>({
|
|
||||||
resolver: zodResolver(zUpdatePromptSchema),
|
|
||||||
defaultValues: {
|
|
||||||
promptId: prompt.id,
|
|
||||||
text: prompt.text,
|
|
||||||
appliesTo: prompt.appliesTo,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
className="flex gap-2"
|
|
||||||
onSubmit={form.handleSubmit(async (value) => {
|
|
||||||
await updatePrompt(value);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="promptId"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem className="hidden">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Add a custom prompt"
|
|
||||||
type="hidden"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="text"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Add a custom prompt"
|
|
||||||
type="text"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="appliesTo"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem className="flex-0">
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Applies To" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value="all">All</SelectItem>
|
|
||||||
<SelectItem value="text">Text</SelectItem>
|
|
||||||
<SelectItem value="images">Images</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ActionButton
|
|
||||||
loading={isUpdating}
|
|
||||||
variant="secondary"
|
|
||||||
type="submit"
|
|
||||||
className="items-center"
|
|
||||||
>
|
|
||||||
<Save className="mr-2 size-4" />
|
|
||||||
Save
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
loading={isDeleting}
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => deletePrompt({ promptId: prompt.id })}
|
|
||||||
className="items-center"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 size-4" />
|
|
||||||
Delete
|
|
||||||
</ActionButton>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TaggingRules() {
|
|
||||||
const { data: prompts, isLoading } = api.prompts.list.useQuery();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-2 flex flex-col gap-2">
|
|
||||||
<div className="w-full text-xl font-medium sm:w-1/3">Tagging Rules</div>
|
|
||||||
<p className="mb-1 text-xs italic text-muted-foreground">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
{isLoading && <FullPageSpinner />}
|
|
||||||
{prompts && prompts.length == 0 && (
|
|
||||||
<p className="rounded-md bg-muted p-2 text-sm text-muted-foreground">
|
|
||||||
You don't have any custom prompts yet.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{prompts &&
|
|
||||||
prompts.map((prompt) => <PromptRow key={prompt.id} prompt={prompt} />)}
|
|
||||||
<PromptEditor />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PromptDemo() {
|
|
||||||
const { data: prompts } = api.prompts.list.useQuery();
|
|
||||||
const clientConfig = useClientConfig();
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="mb-4 w-full text-xl font-medium sm:w-1/3">
|
|
||||||
Prompt Preview
|
|
||||||
</div>
|
|
||||||
<p>Text Prompt</p>
|
|
||||||
<code className="whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
|
||||||
{buildTextPrompt(
|
|
||||||
clientConfig.inference.inferredTagLang,
|
|
||||||
(prompts ?? [])
|
|
||||||
.filter((p) => p.appliesTo == "text" || p.appliesTo == "all")
|
|
||||||
.map((p) => p.text),
|
|
||||||
"\n<CONTENT_HERE>\n",
|
|
||||||
/* context length */ 1024 /* The value here doesn't matter */,
|
|
||||||
).trim()}
|
|
||||||
</code>
|
|
||||||
<p>Image Prompt</p>
|
|
||||||
<code className="whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
|
||||||
{buildImagePrompt(
|
|
||||||
clientConfig.inference.inferredTagLang,
|
|
||||||
(prompts ?? [])
|
|
||||||
.filter((p) => p.appliesTo == "images" || p.appliesTo == "all")
|
|
||||||
.map((p) => p.text),
|
|
||||||
).trim()}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AISettings() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="rounded-md border bg-background p-4">
|
|
||||||
<div className="mb-2 flex flex-col gap-3">
|
|
||||||
<div className="w-full text-2xl font-medium sm:w-1/3">
|
|
||||||
AI Settings
|
|
||||||
</div>
|
|
||||||
<TaggingRules />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 rounded-md border bg-background p-4">
|
|
||||||
<PromptDemo />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -16,7 +16,7 @@ import { api } from "@/lib/trpc";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { zChangePasswordSchema } from "@hoarder/shared/types/users";
|
import { zChangePasswordSchema } from "@lifetracker/shared/types/users";
|
||||||
|
|
||||||
export function ChangePassword() {
|
export function ChangePassword() {
|
||||||
const form = useForm<z.infer<typeof zChangePasswordSchema>>({
|
const form = useForm<z.infer<typeof zChangePasswordSchema>>({
|
||||||
|
|||||||
@ -48,223 +48,8 @@ export function ExportButton() {
|
|||||||
export function ImportExportRow() {
|
export function ImportExportRow() {
|
||||||
const router = useRouter();
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div>TBD!</div>
|
||||||
<div className="flex flex-row flex-wrap gap-2">
|
|
||||||
<FilePickerButton
|
|
||||||
loading={false}
|
|
||||||
accept=".html"
|
|
||||||
multiple={false}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onFileSelect={(file) =>
|
|
||||||
runUploadBookmarkFile({ file, source: "html" })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Upload />
|
|
||||||
<p>Import Bookmarks from HTML file</p>
|
|
||||||
</FilePickerButton>
|
|
||||||
|
|
||||||
<FilePickerButton
|
|
||||||
loading={false}
|
|
||||||
accept=".csv"
|
|
||||||
multiple={false}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onFileSelect={(file) =>
|
|
||||||
runUploadBookmarkFile({ file, source: "pocket" })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Upload />
|
|
||||||
<p>Import Bookmarks from Pocket export</p>
|
|
||||||
</FilePickerButton>
|
|
||||||
<FilePickerButton
|
|
||||||
loading={false}
|
|
||||||
accept=".json"
|
|
||||||
multiple={false}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onFileSelect={(file) =>
|
|
||||||
runUploadBookmarkFile({ file, source: "omnivore" })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Upload />
|
|
||||||
<p>Import Bookmarks from Omnivore export</p>
|
|
||||||
</FilePickerButton>
|
|
||||||
<FilePickerButton
|
|
||||||
loading={false}
|
|
||||||
accept=".json"
|
|
||||||
multiple={false}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onFileSelect={(file) =>
|
|
||||||
runUploadBookmarkFile({ file, source: "hoarder" })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Upload />
|
|
||||||
<p>Import Bookmarks from Hoarder export</p>
|
|
||||||
</FilePickerButton>
|
|
||||||
<ExportButton />
|
|
||||||
</div>
|
|
||||||
{importProgress && (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p className="shrink-0 text-sm">
|
|
||||||
Processed {importProgress.done} of {importProgress.total} bookmarks
|
|
||||||
</p>
|
|
||||||
<div className="w-full">
|
|
||||||
<Progress
|
|
||||||
value={(importProgress.done * 100) / importProgress.total}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { redirect } from "next/navigation";
|
|||||||
import SidebarItem from "@/components/shared/sidebar/SidebarItem";
|
import SidebarItem from "@/components/shared/sidebar/SidebarItem";
|
||||||
import { getServerAuthSession } from "@/server/auth";
|
import { getServerAuthSession } from "@/server/auth";
|
||||||
|
|
||||||
import serverConfig from "@hoarder/shared/config";
|
import serverConfig from "@lifetracker/shared/config";
|
||||||
|
|
||||||
import { settingsSidebarItems } from "./items";
|
import { settingsSidebarItems } from "./items";
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import {
|
|||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Download,
|
Download,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Rss,
|
|
||||||
Sparkles,
|
|
||||||
User,
|
User,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
@ -16,23 +14,13 @@ export const settingsSidebarItems: {
|
|||||||
{
|
{
|
||||||
name: "Back To App",
|
name: "Back To App",
|
||||||
icon: <ArrowLeft size={18} />,
|
icon: <ArrowLeft size={18} />,
|
||||||
path: "/dashboard/bookmarks",
|
path: "/dashboard/today",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "User Info",
|
name: "User Info",
|
||||||
icon: <User size={18} />,
|
icon: <User size={18} />,
|
||||||
path: "/settings/info",
|
path: "/settings/info",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "AI Settings",
|
|
||||||
icon: <Sparkles size={18} />,
|
|
||||||
path: "/settings/ai",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "RSS Subscriptions",
|
|
||||||
icon: <Rss size={18} />,
|
|
||||||
path: "/settings/feeds",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Import / Export",
|
name: "Import / Export",
|
||||||
icon: <Download size={18} />,
|
icon: <Download size={18} />,
|
||||||
@ -43,4 +31,4 @@ export const settingsSidebarItems: {
|
|||||||
icon: <KeyRound size={18} />,
|
icon: <KeyRound size={18} />,
|
||||||
path: "/settings/api-keys",
|
path: "/settings/api-keys",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -17,6 +17,11 @@ export async function createContextFromRequest(req: Request) {
|
|||||||
if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
|
if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
|
||||||
const token = authorizationHeader.split(" ")[1];
|
const token = authorizationHeader.split(" ")[1];
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
console.log("\n\nEeeeeeentering hell\n\n");
|
||||||
|
console.log(await authenticateApiKey(token));
|
||||||
|
console.log("\n\n\n\n");
|
||||||
|
|
||||||
const user = await authenticateApiKey(token);
|
const user = await authenticateApiKey(token);
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
|||||||
@ -8,9 +8,7 @@ import path from "path";
|
|||||||
import dbConfig from "./drizzle.config";
|
import dbConfig from "./drizzle.config";
|
||||||
|
|
||||||
const sqlite = new Database(dbConfig.dbCredentials.url);
|
const sqlite = new Database(dbConfig.dbCredentials.url);
|
||||||
|
export const db = drizzle(sqlite, { schema, logger: true });
|
||||||
console.log(dbConfig.dbCredentials.url);
|
|
||||||
export const db = drizzle(sqlite, { schema });
|
|
||||||
|
|
||||||
export function getInMemoryDB(runMigrations: boolean) {
|
export function getInMemoryDB(runMigrations: boolean) {
|
||||||
const mem = new Database(":memory:");
|
const mem = new Database(":memory:");
|
||||||
|
|||||||
4
packages/db/migrations/0004_sad_sally_floyd.sql
Normal file
4
packages/db/migrations/0004_sad_sally_floyd.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE `config` (
|
||||||
|
`key` text PRIMARY KEY NOT NULL,
|
||||||
|
`value` text NOT NULL
|
||||||
|
);
|
||||||
423
packages/db/migrations/meta/0004_snapshot.json
Normal file
423
packages/db/migrations/meta/0004_snapshot.json
Normal file
@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,13 @@
|
|||||||
"when": 1731542202120,
|
"when": 1731542202120,
|
||||||
"tag": "0003_complex_ares",
|
"tag": "0003_complex_ares",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731637559763,
|
||||||
|
"tag": "0004_sad_sally_floyd",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -104,3 +104,17 @@ export const days = sqliteTable("day", {
|
|||||||
date: text("date").notNull().unique(),
|
date: text("date").notNull().unique(),
|
||||||
comment: text("comment").notNull(),
|
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],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|||||||
@ -54,7 +54,21 @@ function parseApiKey(plain: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function authenticateApiKey(key: string) {
|
export async function authenticateApiKey(key: string) {
|
||||||
|
|
||||||
const { keyId, keySecret } = parseApiKey(key);
|
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({
|
const apiKey = await db.query.apiKeys.findFirst({
|
||||||
where: (k, { eq }) => eq(k.keyId, keyId),
|
where: (k, { eq }) => eq(k.keyId, keyId),
|
||||||
with: {
|
with: {
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
import { apiKeys } from "@lifetracker/db/schema";
|
||||||
import { router } from "../index";
|
import { router } from "../index";
|
||||||
import { usersAppRouter } from "./users";
|
import { usersAppRouter } from "./users";
|
||||||
|
import { apiKeysAppRouter } from "./apiKeys";
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
users: usersAppRouter
|
users: usersAppRouter,
|
||||||
|
apiKeys: apiKeysAppRouter,
|
||||||
});
|
});
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
114
packages/trpc/routers/apiKeys.ts
Normal file
114
packages/trpc/routers/apiKeys.ts
Normal file
@ -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("<unknown>", error.message, ctx.req.ip);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user