aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-03-19 00:33:11 +0000
committerGitHub <noreply@github.com>2024-03-19 00:33:11 +0000
commit785a5b574992296e187a66412dd42f7b4a686353 (patch)
tree64b608927cc63d7494395f639636fd4b36e5a977 /apps/web/app
parent549520919c482e72cdf7adae5ba852d1b6cbe5aa (diff)
downloadkarakeep-785a5b574992296e187a66412dd42f7b4a686353.tar.zst
Feature: Add support for uploading images and automatically inferring their tags (#2)
* feature: Experimental support for asset uploads * feature(web): Add new bookmark type asset * feature: Add support for automatically tagging images * fix: Add support for image assets in preview page * use next Image for fetching the images * Fix auth and error codes in the route handlers * Add support for image uploads on mobile * Fix typing of upload requests * Remove the ugly dragging box * Bump mobile version to 1.3 * Change the editor card placeholder to mention uploading images * Fix a typo * Change ios icon for photo library * Silence typescript error
Diffstat (limited to 'apps/web/app')
-rw-r--r--apps/web/app/api/assets/[assetId]/route.ts29
-rw-r--r--apps/web/app/api/assets/route.ts52
-rw-r--r--apps/web/app/api/trpc/[trpc]/route.ts19
-rw-r--r--apps/web/app/dashboard/layout.tsx3
4 files changed, 85 insertions, 18 deletions
diff --git a/apps/web/app/api/assets/[assetId]/route.ts b/apps/web/app/api/assets/[assetId]/route.ts
new file mode 100644
index 00000000..6b583e51
--- /dev/null
+++ b/apps/web/app/api/assets/[assetId]/route.ts
@@ -0,0 +1,29 @@
+import { createContextFromRequest } from "@/server/api/client";
+import { and, eq } from "drizzle-orm";
+
+import { db } from "@hoarder/db";
+import { assets } from "@hoarder/db/schema";
+
+export const dynamic = "force-dynamic";
+export async function GET(
+ request: Request,
+ { params }: { params: { assetId: string } },
+) {
+ const ctx = await createContextFromRequest(request);
+ if (!ctx.user) {
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
+ }
+ const asset = await db.query.assets.findFirst({
+ where: and(eq(assets.id, params.assetId), eq(assets.userId, ctx.user.id)),
+ });
+
+ if (!asset) {
+ return Response.json({ error: "Asset not found" }, { status: 404 });
+ }
+ return new Response(asset.blob as string, {
+ status: 200,
+ headers: {
+ "Content-type": asset.contentType,
+ },
+ });
+}
diff --git a/apps/web/app/api/assets/route.ts b/apps/web/app/api/assets/route.ts
new file mode 100644
index 00000000..2caa4d4c
--- /dev/null
+++ b/apps/web/app/api/assets/route.ts
@@ -0,0 +1,52 @@
+import { createContextFromRequest } from "@/server/api/client";
+
+import type { ZUploadResponse } from "@hoarder/trpc/types/uploads";
+import { db } from "@hoarder/db";
+import { assets } from "@hoarder/db/schema";
+
+const SUPPORTED_ASSET_TYPES = new Set(["image/jpeg", "image/png"]);
+
+const MAX_UPLOAD_SIZE_BYTES = 4 * 1024 * 1024;
+
+export const dynamic = "force-dynamic";
+export async function POST(request: Request) {
+ const ctx = await createContextFromRequest(request);
+ if (!ctx.user) {
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
+ }
+ const formData = await request.formData();
+ const data = formData.get("image");
+ let buffer;
+ let contentType;
+ if (data instanceof File) {
+ contentType = data.type;
+ if (!SUPPORTED_ASSET_TYPES.has(contentType)) {
+ return Response.json(
+ { error: "Unsupported asset type" },
+ { status: 400 },
+ );
+ }
+ if (data.size > MAX_UPLOAD_SIZE_BYTES) {
+ return Response.json({ error: "Asset is too big" }, { status: 413 });
+ }
+ buffer = Buffer.from(await data.arrayBuffer());
+ } else {
+ return Response.json({ error: "Bad request" }, { status: 400 });
+ }
+
+ const [dbRes] = await db
+ .insert(assets)
+ .values({
+ encoding: "binary",
+ contentType: contentType,
+ blob: buffer,
+ userId: ctx.user.id,
+ })
+ .returning();
+
+ return Response.json({
+ assetId: dbRes.id,
+ contentType: dbRes.contentType,
+ size: buffer.byteLength,
+ } satisfies ZUploadResponse);
+}
diff --git a/apps/web/app/api/trpc/[trpc]/route.ts b/apps/web/app/api/trpc/[trpc]/route.ts
index 23df286f..1afcb886 100644
--- a/apps/web/app/api/trpc/[trpc]/route.ts
+++ b/apps/web/app/api/trpc/[trpc]/route.ts
@@ -1,8 +1,6 @@
-import { createContext } from "@/server/api/client";
+import { createContextFromRequest } from "@/server/api/client";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
-import { db } from "@hoarder/db";
-import { authenticateApiKey } from "@hoarder/trpc/auth";
import { appRouter } from "@hoarder/trpc/routers/_app";
const handler = (req: Request) =>
@@ -18,20 +16,7 @@ const handler = (req: Request) =>
},
createContext: async (opts) => {
- // 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 authorizationHeader = opts.req.headers.get("Authorization");
- if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
- const token = authorizationHeader.split(" ")[1];
- try {
- const user = await authenticateApiKey(token);
- return { user, db };
- } catch (e) {
- // Fallthrough to cookie-based auth
- }
- }
-
- return createContext();
+ return await createContextFromRequest(opts.req);
},
});
export { handler as GET, handler as POST };
diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx
index dc3af9c7..27e06955 100644
--- a/apps/web/app/dashboard/layout.tsx
+++ b/apps/web/app/dashboard/layout.tsx
@@ -1,5 +1,6 @@
import MobileSidebar from "@/components/dashboard/sidebar/ModileSidebar";
import Sidebar from "@/components/dashboard/sidebar/Sidebar";
+import UploadDropzone from "@/components/dashboard/UploadDropzone";
import { Separator } from "@/components/ui/separator";
export default async function Dashboard({
@@ -17,7 +18,7 @@ export default async function Dashboard({
<MobileSidebar />
<Separator />
</div>
- {children}
+ <UploadDropzone>{children}</UploadDropzone>
</main>
</div>
);