Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/api/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextRequest } from "next/server";
import { elysiaApi } from "@/lib/api/elysia/app";

async function handle(request: NextRequest): Promise<Response> {
return elysiaApi.handle(request);
}

export const GET = handle;
export const POST = handle;
export const PUT = handle;
export const PATCH = handle;
export const DELETE = handle;
export const HEAD = handle;
export const OPTIONS = handle;

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
32 changes: 0 additions & 32 deletions app/api/agent-templates/favorites/route.ts

This file was deleted.

72 changes: 0 additions & 72 deletions app/api/youtube/channel-info/route.ts

This file was deleted.

5 changes: 1 addition & 4 deletions components/Agents/useAgentToggleFavorite.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { useUserProvider } from "@/providers/UserProvder";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import type { ToggleFavoriteRequest } from "@/types/AgentTemplates";

export function useAgentToggleFavorite() {
const { userData } = useUserProvider();
const queryClient = useQueryClient();

const handleToggleFavorite = async (
templateId: string,
nextFavourite: boolean
) => {
if (!userData?.id || !templateId) return;
if (!templateId) return;

try {
const body: ToggleFavoriteRequest = {
templateId,
userId: userData.id,
isFavourite: nextFavourite,
};
const res = await fetch("/api/agent-templates/favorites", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Youtube } from "lucide-react";
const ChannelInfo = ({ dense, artistAccountId }: { dense?: boolean; artistAccountId: string }) => {
const { data, isLoading } = useYoutubeChannel(artistAccountId);

const channel = data?.channels?.[0];
const channel = data?.[0];

return (
<div className="flex flex-col gap-1 cursor-pointer">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const ChatInputYoutubeButtonPopover = ({ children, artistAccountId }: { children
const { data: channelInfo, isLoading: isChannelInfoLoading } = useYoutubeChannel(artistAccountId);
const isMobile = useIsMobile();
const [isOpen, setIsOpen] = useState(false);
const channel = channelInfo?.channels?.[0];
const channel = channelInfo?.[0];

if (youtubeStatus?.status === "invalid" || isLoading || isChannelInfoLoading) {
return children;
Expand Down
12 changes: 9 additions & 3 deletions hooks/useYouTubeLoginSuccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ export function useYouTubeLoginSuccess() {
hasCheckedOAuth.current = true;

if (selectedArtist?.account_id) {
fetchYouTubeChannel(selectedArtist.account_id).then((youtubeChannel) => {
if (youtubeChannel.success) {
const checkYouTubeConnection = async () => {
try {
await fetchYouTubeChannel(selectedArtist.account_id);

const successMessage = {
id: generateUUID(),
role: "user" as const,
Expand All @@ -59,8 +61,12 @@ export function useYouTubeLoginSuccess() {
} as UIMessage;

append(successMessage);
} catch {
// Ignore auth/check errors here; normal flow continues.
}
});
};

void checkYouTubeConnection();
}
}, [messages, append, selectedArtist?.account_id]);
}
5 changes: 2 additions & 3 deletions hooks/useYoutubeChannel.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import fetchYouTubeChannel from "@/lib/youtube/fetchYouTubeChannel";
import { YouTubeChannelResponse } from "@/types/youtube";
import { useQuery } from "@tanstack/react-query";
import fetchYouTubeChannel from "@/lib/youtube/fetchYouTubeChannel";

const useYoutubeChannel = (artistAccountId: string) => {
return useQuery<YouTubeChannelResponse>({
return useQuery({
queryKey: ["youtube-channel-info", artistAccountId],
queryFn: () => fetchYouTubeChannel(artistAccountId),
enabled: !!artistAccountId, // Only run query if artistAccountId is provided
Expand Down
8 changes: 3 additions & 5 deletions hooks/useYoutubeStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ const useYoutubeStatus = (artistAccountId?: string) => {
status: (() => {
if (error) return "error";
if (isLoading) return "invalid";
if (channelResponse) {
return channelResponse.tokenStatus === "valid"
? "valid"
: "invalid";
if (Array.isArray(channelResponse) && channelResponse.length > 0) {
return "valid";
}
return "invalid";
})(),
Expand All @@ -27,7 +25,7 @@ const useYoutubeStatus = (artistAccountId?: string) => {
return {
data,
isLoading,
error: null,
error: error ?? null,
} as YoutubeStatus;
};

Expand Down
32 changes: 32 additions & 0 deletions lib/api/elysia/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Elysia } from "elysia";
import { openapi } from "@elysiajs/openapi";
import { authPlugin } from "@/lib/api/elysia/plugins/auth";
import { elysiaRoutes } from "@/lib/api/elysia/routes";

export const elysiaApi = new Elysia({ prefix: "/api" })
.use(
openapi({
path: "/openapi",
specPath: "/openapi/json",
provider: "scalar",
documentation: {
info: {
title: "Recoupable Chat API (Elysia POC)",
version: "0.1.0",
},
components: {
securitySchemes: {
apiKeyAuth: {
type: "apiKey",
in: "header",
name: "x-api-key",
},
},
},
},
}),
)
.use(authPlugin)
.use(elysiaRoutes);

export type ElysiaApi = typeof elysiaApi;
10 changes: 10 additions & 0 deletions lib/api/elysia/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { treaty } from "@elysiajs/eden";
import type { ElysiaApi } from "@/lib/api/elysia/app";

const client = treaty<ElysiaApi>("", {
fetch: {
credentials: "include",
},
});

export const api = client.api;
45 changes: 45 additions & 0 deletions lib/api/elysia/plugins/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Elysia } from "elysia";
import { getAccountIdFromPrivyToken } from "@/lib/auth/getAccountIdFromPrivyToken";
import { getAccountIdFromApiKey } from "@/lib/supabase/account_api_keys/getAccountIdFromApiKey";

export const authPlugin = new Elysia({ name: "auth-plugin" })
.derive({as: "global"}, async ({ cookie, request }) => {
const apiKey = request.headers.get("x-api-key");
const privyToken = cookie["privy-token"].value as string;

let user: { userId: string; identifier: string } | null = null;

if (apiKey) {
try {
user = {
userId: await getAccountIdFromApiKey(apiKey),
identifier: apiKey,
};
Comment on lines +14 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not store raw API keys/tokens in request context.

identifier currently contains the raw API key or Privy token. Keep only non-secret identity fields (e.g., userId, authMethod) to reduce credential exposure risk.

🔧 Suggested fix
-    let user: { userId: string; identifier: string } | null = null;
+    let user: { userId: string; authMethod: "apiKey" | "privy" } | null = null;
@@
         user = {
           userId: await getAccountIdFromApiKey(apiKey),
-          identifier: apiKey,
+          authMethod: "apiKey",
         };
@@
           user = {
             userId: accountId,
-            identifier: privyToken,
+            authMethod: "privy",
           };

As per coding guidelines, "Implement built-in security practices for authentication and data handling."

Also applies to: 25-28

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/elysia/plugins/auth.ts` around lines 14 - 17, The code in the user
construction (user = { userId: await getAccountIdFromApiKey(apiKey), identifier:
apiKey }) stores the raw API key/token in the request context; remove the raw
secret from the context and replace it with a non-secret identifier such as
authMethod and/or a derived non-sensitive ID (e.g., tokenId, the account ID, or
a hashed/truncated fingerprint) instead; update the same pattern used around
lines 25-28 so functions like getAccountIdFromApiKey are used to populate userId
while identifier no longer holds the raw apiKey/Privy token.

} catch (error) {
console.error("Elysia auth apiKey validation failed:", error);
}
} else if (privyToken) {
try {
const accountId = await getAccountIdFromPrivyToken(privyToken);
if (accountId) {
user = {
userId: accountId,
identifier: privyToken,
};
}
} catch (error) {
console.error("Elysia auth privyToken validation failed:", error);
}
}

return { user };
})
.macro({
auth: {
beforeHandle({ user, status }) {
if (!user) {
return status(401, "Authentication required");
}
},
},
});
44 changes: 44 additions & 0 deletions lib/api/elysia/routes/agentTemplates/favorites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Elysia, t } from "elysia";
import { addAgentTemplateFavorite } from "@/lib/supabase/agent_templates/addAgentTemplateFavorite";
import { removeAgentTemplateFavorite } from "@/lib/supabase/agent_templates/removeAgentTemplateFavorite";

const errorResponseSchema = t.String();

export const agentTemplateFavoritesRoute = new Elysia().post(
"/favorites",
async ({body, status, user}) => {
try {
if (body.isFavourite) {
await addAgentTemplateFavorite(body.templateId, user.userId);
} else {
await removeAgentTemplateFavorite(body.templateId, user.userId);
}

return { success: true as const };
} catch (error) {
console.error("Error toggling favourite:", error);
return status(500, "Failed to toggle favourite");
}
},
{
auth: true,
body: t.Object({
templateId: t.String(),
isFavourite: t.Boolean(),
}),
response: {
200: t.Object({
success: t.Literal(true),
}),
401: errorResponseSchema,
500: errorResponseSchema,
},
detail: {
summary: "Toggle agent template favorite",
description:
"Adds or removes a template favorite for the authenticated user.",
tags: ["agent-templates"],
security: [{ apiKeyAuth: [] }],
},
},
);
5 changes: 5 additions & 0 deletions lib/api/elysia/routes/agentTemplates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Elysia } from "elysia";
import { agentTemplateFavoritesRoute } from "@/lib/api/elysia/routes/agentTemplates/favorites";

export const agentTemplateRoutes = new Elysia({ prefix: "/agent-templates" })
.use(agentTemplateFavoritesRoute);
5 changes: 5 additions & 0 deletions lib/api/elysia/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Elysia } from "elysia";
import { agentTemplateRoutes } from "@/lib/api/elysia/routes/agentTemplates";
import { youtubeRoutes } from "@/lib/api/elysia/routes/youtube";

export const elysiaRoutes = new Elysia().use(youtubeRoutes).use(agentTemplateRoutes);
Loading
Loading