Skip to content

Commit 1507a6a

Browse files
refactor: simplify elysia auth and flatten youtube response
1 parent 0f6df79 commit 1507a6a

9 files changed

Lines changed: 127 additions & 218 deletions

File tree

components/ArtistSetting/StandaloneYoutubeComponent/ChannelInfo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Youtube } from "lucide-react";
66
const ChannelInfo = ({ dense, artistAccountId }: { dense?: boolean; artistAccountId: string }) => {
77
const { data, isLoading } = useYoutubeChannel(artistAccountId);
88

9-
const channel = data?.channels?.[0];
9+
const channel = data?.[0];
1010

1111
return (
1212
<div className="flex flex-col gap-1 cursor-pointer">

components/YouTube/ChatInputYoutubeButtonPopover/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const ChatInputYoutubeButtonPopover = ({ children, artistAccountId }: { children
1414
const { data: channelInfo, isLoading: isChannelInfoLoading } = useYoutubeChannel(artistAccountId);
1515
const isMobile = useIsMobile();
1616
const [isOpen, setIsOpen] = useState(false);
17-
const channel = channelInfo?.channels?.[0];
17+
const channel = channelInfo?.[0];
1818

1919
if (youtubeStatus?.status === "invalid" || isLoading || isChannelInfoLoading) {
2020
return children;

hooks/useYouTubeLoginSuccess.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useArtistProvider } from "@/providers/ArtistProvider";
33
import { useVercelChatContext } from "@/providers/VercelChatProvider";
44
import { generateUUID } from "@/lib/generateUUID";
55
import { api } from "@/lib/api/elysia/client";
6-
import { unwrapResponse } from "@/lib/api/elysia/unwrapResponse";
76
import { UIMessage, isToolUIPart, getToolName } from "ai";
87

98
/**
@@ -46,34 +45,34 @@ export function useYouTubeLoginSuccess() {
4645
hasCheckedOAuth.current = true;
4746

4847
if (selectedArtist?.account_id) {
49-
api.youtube["channel-info"]
50-
.get({
51-
query: { artist_account_id: selectedArtist.account_id },
52-
})
53-
.then((youtubeChannel) => {
54-
const data = unwrapResponse(
55-
youtubeChannel,
56-
"Failed to fetch YouTube channel information",
57-
);
48+
const checkYouTubeConnection = async () => {
49+
try {
50+
const youtubeChannel = await api.youtube["channel-info"].get({
51+
query: { artist_account_id: selectedArtist.account_id },
52+
});
5853

59-
if (data.success) {
60-
const successMessage = {
61-
id: generateUUID(),
62-
role: "user" as const,
63-
parts: [
64-
{
65-
type: "text",
66-
text: "Great! I've successfully connected my YouTube account. Please continue with what you were helping me with.",
67-
},
68-
],
69-
} as UIMessage;
70-
71-
append(successMessage);
54+
if (youtubeChannel.error || !youtubeChannel.data) {
55+
throw new Error("Failed to fetch YouTube channel information");
7256
}
73-
})
74-
.catch(() => {
57+
58+
const successMessage = {
59+
id: generateUUID(),
60+
role: "user" as const,
61+
parts: [
62+
{
63+
type: "text",
64+
text: "Great! I've successfully connected my YouTube account. Please continue with what you were helping me with.",
65+
},
66+
],
67+
} as UIMessage;
68+
69+
append(successMessage);
70+
} catch {
7571
// Ignore auth/check errors here; normal flow continues.
76-
});
72+
}
73+
};
74+
75+
void checkYouTubeConnection();
7776
}
7877
}, [messages, append, selectedArtist?.account_id]);
7978
}

hooks/useYoutubeChannel.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useQuery } from "@tanstack/react-query";
22
import { api } from "@/lib/api/elysia/client";
3-
import { unwrapResponse } from "@/lib/api/elysia/unwrapResponse";
43

54
const useYoutubeChannel = (artistAccountId: string) => {
65
return useQuery({
@@ -10,10 +9,11 @@ const useYoutubeChannel = (artistAccountId: string) => {
109
query: { artist_account_id: artistAccountId },
1110
});
1211

13-
return unwrapResponse(
14-
response,
15-
"Failed to fetch YouTube channel information",
16-
);
12+
if (response.error || !response.data) {
13+
throw new Error("Failed to fetch YouTube channel information");
14+
}
15+
16+
return response.data;
1717
},
1818
enabled: !!artistAccountId, // Only run query if artistAccountId is provided
1919
});

lib/api/elysia/app.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Elysia } from "elysia";
22
import { openapi } from "@elysiajs/openapi";
3-
import { authMacro } from "@/lib/api/elysia/plugins/auth";
3+
import { authPlugin } from "@/lib/api/elysia/plugins/auth";
44
import { elysiaRoutes } from "@/lib/api/elysia/routes";
55

66
export const elysiaApi = new Elysia({ prefix: "/api" })
@@ -16,11 +16,6 @@ export const elysiaApi = new Elysia({ prefix: "/api" })
1616
},
1717
components: {
1818
securitySchemes: {
19-
bearerAuth: {
20-
type: "http",
21-
scheme: "bearer",
22-
bearerFormat: "JWT",
23-
},
2419
apiKeyAuth: {
2520
type: "apiKey",
2621
in: "header",
@@ -31,7 +26,7 @@ export const elysiaApi = new Elysia({ prefix: "/api" })
3126
},
3227
}),
3328
)
34-
.use(authMacro)
29+
.use(authPlugin)
3530
.use(elysiaRoutes);
3631

3732
export type ElysiaApi = typeof elysiaApi;

lib/api/elysia/plugins/auth.ts

Lines changed: 44 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { createHmac } from "crypto";
21
import { PrivyClient } from "@privy-io/node";
32
import { Elysia } from "elysia";
4-
import supabase from "@/lib/supabase/serverClient";
3+
import getAccountDetailsByEmails from "@/lib/supabase/account_emails/getAccountDetailsByEmails";
4+
import { getAccountIdFromApiKey } from "@/lib/supabase/account_api_keys/getAccountIdFromApiKey";
55

66
const PRIVY_APP_ID = process.env.PRIVY_APP_ID || process.env.NEXT_PUBLIC_PRIVY_APP_ID;
77
const PRIVY_PROJECT_SECRET = process.env.PRIVY_PROJECT_SECRET;
@@ -16,127 +16,49 @@ const privyClient =
1616
})
1717
: null;
1818

19-
function hashApiKey(rawKey: string): string {
20-
if (!PRIVY_PROJECT_SECRET) {
21-
throw new Error("Missing PRIVY_PROJECT_SECRET");
22-
}
23-
24-
return createHmac("sha256", PRIVY_PROJECT_SECRET).update(rawKey).digest("hex");
25-
}
26-
27-
function parseCookies(cookieHeader: string | null): Record<string, string> {
28-
if (!cookieHeader) return {};
29-
30-
return cookieHeader
31-
.split(";")
32-
.map((part) => part.trim())
33-
.filter(Boolean)
34-
.reduce<Record<string, string>>((acc, part) => {
35-
const separatorIndex = part.indexOf("=");
36-
if (separatorIndex <= 0) return acc;
37-
38-
const key = part.slice(0, separatorIndex).trim();
39-
const value = part.slice(separatorIndex + 1).trim();
19+
export const authPlugin = new Elysia({ name: "auth-plugin" })
20+
.derive(async ({ cookie, request }) => {
21+
const apiKey = request.headers.get("x-api-key");
22+
const privyToken = cookie["privy-token"].value as string;
23+
24+
let user: { userId: string; identifier: string } | null = null;
4025

26+
if (apiKey) {
4127
try {
42-
acc[key] = decodeURIComponent(value);
43-
} catch {
44-
acc[key] = value;
28+
user = {
29+
userId: await getAccountIdFromApiKey(apiKey),
30+
identifier: apiKey,
31+
};
32+
} catch (error) {
33+
console.error("Elysia auth apiKey validation failed:", error);
4534
}
46-
47-
return acc;
48-
}, {});
49-
}
50-
51-
function getPrivyAuthTokenFromRequest(request: Request): string | null {
52-
const cookies = parseCookies(request.headers.get("cookie"));
53-
return cookies["privy-token"] || null;
54-
}
55-
56-
async function getAccountIdFromApiKey(apiKey: string): Promise<string | null> {
57-
const keyHash = hashApiKey(apiKey);
58-
59-
const { data, error } = await supabase
60-
.from("account_api_keys")
61-
.select("account")
62-
.eq("key_hash", keyHash)
63-
.limit(1)
64-
.maybeSingle();
65-
66-
if (error) {
67-
throw new Error(`Failed API key lookup: ${error.message}`);
68-
}
69-
70-
return data?.account ?? null;
71-
}
72-
73-
function getEmailFromPrivyUser(user: { linked_accounts?: Array<{ type?: string; address?: string }> }) {
74-
const emailAccount = user.linked_accounts?.find((account) => account.type === "email");
75-
return emailAccount?.address?.toLowerCase() ?? null;
76-
}
77-
78-
async function getAccountIdFromPrivyAuthToken(authToken: string): Promise<string | null> {
79-
if (!privyClient) {
80-
throw new Error(
81-
"Missing Privy configuration: PRIVY_APP_ID/NEXT_PUBLIC_PRIVY_APP_ID, PRIVY_PROJECT_SECRET, and PRIVY_JWT_VERIFICATION_KEY are required",
82-
);
83-
}
84-
85-
const verified = await privyClient.utils().auth().verifyAuthToken(authToken);
86-
const user = await privyClient.users()._get(verified.user_id);
87-
const email = getEmailFromPrivyUser(user);
88-
89-
if (!email) {
90-
throw new Error("No email found in authenticated Privy user");
91-
}
92-
93-
const { data, error } = await supabase
94-
.from("account_emails")
95-
.select("account_id")
96-
.eq("email", email)
97-
.limit(1)
98-
.maybeSingle();
99-
100-
if (error) {
101-
throw new Error(`Failed account email lookup: ${error.message}`);
102-
}
103-
104-
return data?.account_id ?? null;
105-
}
106-
107-
export const authMacro = new Elysia({ name: "auth-macro" }).macro({
108-
auth(enabled: boolean) {
109-
if (!enabled) return;
110-
111-
return {
112-
async beforeHandle({ request, status }) {
113-
const apiKey = request.headers.get("x-api-key");
114-
115-
try {
116-
if (apiKey) {
117-
const accountId = await getAccountIdFromApiKey(apiKey);
118-
if (!accountId) {
119-
return status(401, { error: "Unauthorized" });
120-
}
121-
return;
122-
}
123-
124-
const privyAuthToken = getPrivyAuthTokenFromRequest(request);
125-
if (!privyAuthToken) {
126-
return status(401, { error: "Authentication required" });
127-
}
128-
129-
const accountId = await getAccountIdFromPrivyAuthToken(privyAuthToken);
130-
if (!accountId) {
131-
return status(401, { error: "Unauthorized" });
132-
}
133-
} catch (error) {
134-
return status(401, {
135-
error: "Authentication failed",
136-
details: error instanceof Error ? error.message : "Unknown authentication error",
137-
});
35+
} else if (privyToken && privyClient) {
36+
try {
37+
const verified = await privyClient.utils().auth().verifyAccessToken(privyToken);
38+
const privyUser = await privyClient.users()._get(verified.user_id);
39+
const email = privyUser.linked_accounts?.find((account) => account.type === "email")?.address;
40+
const accountDetails = email ? await getAccountDetailsByEmails([email]) : [];
41+
const accountId = accountDetails[0]?.account_id ?? null;
42+
43+
if (accountId) {
44+
user = {
45+
userId: accountId,
46+
identifier: privyToken,
47+
};
13848
}
139-
},
140-
};
141-
},
142-
});
49+
} catch (error) {
50+
console.error("Elysia auth privyToken validation failed:", error);
51+
}
52+
}
53+
54+
return { user };
55+
})
56+
.macro({
57+
auth: {
58+
beforeHandle({ user, status }) {
59+
if (!user) {
60+
return status(401, "Authentication required");
61+
}
62+
},
63+
},
64+
});

lib/api/elysia/routes/youtube/channelInfo.ts

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,20 @@ import { Elysia, t } from "elysia";
22
import { fetchYouTubeChannelInfo } from "@/lib/youtube/channel-fetcher";
33
import { validateYouTubeTokens } from "@/lib/youtube/token-validator";
44

5-
const errorResponseSchema = t.Object({
6-
error: t.String(),
7-
details: t.Optional(t.String()),
8-
});
5+
const errorResponseSchema = t.String();
96

107
export const youtubeChannelInfoRoute = new Elysia().get(
118
"/channel-info",
129
async ({ query, status }) => {
1310
if (!query.artist_account_id) {
14-
return status(400, { error: "Missing artist_account_id parameter" });
11+
return status(400, "Missing artist_account_id parameter");
1512
}
1613

1714
try {
1815
const tokenValidation = await validateYouTubeTokens(query.artist_account_id);
1916

2017
if (!tokenValidation.success || !tokenValidation.tokens) {
21-
return status(403, { error: "YouTube authentication required" });
18+
return status(403, "YouTube authentication required");
2219
}
2320

2421
const channelResult = await fetchYouTubeChannelInfo({
@@ -28,18 +25,17 @@ export const youtubeChannelInfoRoute = new Elysia().get(
2825
});
2926

3027
if (!channelResult.success) {
31-
return status(502, { error: channelResult.error.message });
28+
return status(502, channelResult.error.message);
3229
}
3330

34-
return {
35-
success: true as const,
36-
channels: channelResult.channelData,
37-
};
31+
return channelResult.channelData;
3832
} catch (e) {
39-
return status(500, {
40-
error: "Failed to fetch YouTube channel information",
41-
details: e instanceof Error ? e.message : "Unknown error",
42-
});
33+
return status(
34+
500,
35+
e instanceof Error
36+
? `Failed to fetch YouTube channel information: ${e.message}`
37+
: "Failed to fetch YouTube channel information",
38+
);
4339
}
4440
},
4541
{
@@ -48,10 +44,7 @@ export const youtubeChannelInfoRoute = new Elysia().get(
4844
artist_account_id: t.Optional(t.String()),
4945
}),
5046
response: {
51-
200: t.Object({
52-
success: t.Literal(true),
53-
channels: t.Array(t.Any()),
54-
}),
47+
200: t.Array(t.Any()),
5548
400: errorResponseSchema,
5649
401: errorResponseSchema,
5750
403: errorResponseSchema,
@@ -63,7 +56,7 @@ export const youtubeChannelInfoRoute = new Elysia().get(
6356
description:
6457
"Returns authenticated YouTube channels for an artist account with validated credentials.",
6558
tags: ["youtube"],
66-
security: [{ bearerAuth: [] }, { apiKeyAuth: [] }],
59+
security: [{ apiKeyAuth: [] }],
6760
},
6861
},
6962
);

0 commit comments

Comments
 (0)