diff --git a/app/(auth)/auth/callback/route.ts b/app/(auth)/auth/callback/route.ts
index 0a3d844..9a4798e 100644
--- a/app/(auth)/auth/callback/route.ts
+++ b/app/(auth)/auth/callback/route.ts
@@ -5,7 +5,7 @@ import {
HttpBadRequestError,
HttpInternalServerError,
} from '@/lib/errors';
-import { saveUserInfo } from '@/lib/services/spotify/save-user-info';
+import { updateAccount } from '@/lib/services/account';
import { createSupabaseServerClient } from '@/lib/services/supabase/server';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
@@ -14,7 +14,6 @@ import { ZodError } from 'zod';
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
- const redirect = requestUrl.searchParams.get('redirect');
if (!code) {
return new HttpBadRequestError({
@@ -35,18 +34,10 @@ export async function GET(request: NextRequest) {
}
try {
- const { isNewAccount } = await saveUserInfo({ session, user });
-
- if (isNewAccount) {
- return NextResponse.redirect(
- new URL('/onboarding/start', request.url).toString(),
- {
- status: 301,
- },
- );
- }
-
- return NextResponse.redirect(new URL(redirect || '/shows', request.url));
+ await updateAccount({ session, user });
+ const redirect = requestUrl.searchParams.get('redirect');
+ const redirectURL = new URL(redirect || '/shows', request.url);
+ return NextResponse.redirect(redirectURL);
} catch (error) {
switch (true) {
case error instanceof DatabaseError:
diff --git a/app/(auth)/auth/sign-in/route.ts b/app/(auth)/auth/sign-in/[:provider]/route.ts
similarity index 57%
rename from app/(auth)/auth/sign-in/route.ts
rename to app/(auth)/auth/sign-in/[:provider]/route.ts
index 6c8ccf6..5c9c961 100644
--- a/app/(auth)/auth/sign-in/route.ts
+++ b/app/(auth)/auth/sign-in/[:provider]/route.ts
@@ -1,3 +1,4 @@
+import type { SignInWithOAuthCredentials } from '@supabase/supabase-js';
import type { NextRequest } from 'next/server';
import { HttpAuthenticationError } from '@/lib/errors';
@@ -5,23 +6,43 @@ import { createSupabaseServerClient } from '@/lib/services/supabase/server';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
-export async function POST(request: NextRequest) {
- const supabase = createSupabaseServerClient(cookies());
+const getOAuthConfig = (
+ provider: string,
+): {
+ provider: SignInWithOAuthCredentials['provider'];
+ scopes?: string;
+} => {
+ switch (provider) {
+ case 'spotify':
+ return {
+ provider,
+ scopes: 'user-read-email user-read-playback-position user-library-read',
+ };
+
+ default:
+ return {
+ provider: 'github',
+ };
+ }
+};
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { provider: string } },
+) {
const formData = await request.formData();
const redirect = String(formData.get('redirect'));
-
const redirectURL = new URL('/auth/callback', request.url);
redirectURL.searchParams.set('redirect', redirect);
- const scopes =
- 'user-read-email user-read-playback-position user-library-read';
+ const supabase = createSupabaseServerClient(cookies());
+ const { provider, scopes } = getOAuthConfig(params.provider);
const result = await supabase.auth.signInWithOAuth({
options: {
redirectTo: redirectURL.toString(),
scopes,
},
- provider: 'spotify',
+ provider,
});
if (result.error) {
diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx
index 2cfe8fc..d2f671f 100644
--- a/app/(auth)/sign-in/page.tsx
+++ b/app/(auth)/sign-in/page.tsx
@@ -1,5 +1,10 @@
+import type { SignInWithOAuthCredentials } from '@supabase/supabase-js';
+
+import { ENABLED_OAUTH_PROVIDERS } from '@/lib/services/supabase/auth';
import { Button, Flex, Heading, Link, Text } from '@radix-ui/themes';
+import { CgLogIn } from 'react-icons/cg';
import { FaSpotify } from 'react-icons/fa';
+import { FaGithub } from 'react-icons/fa6';
type Props = {
searchParams?: {
@@ -7,6 +12,36 @@ type Props = {
};
};
+function AuthProviderLabel(props: {
+ provider: SignInWithOAuthCredentials['provider'];
+}) {
+ switch (props.provider) {
+ case 'spotify':
+ return 'Spotify';
+
+ case 'github':
+ return 'GitHub';
+
+ default:
+ return props.provider;
+ }
+}
+
+function AuthProviderIcon(props: {
+ provider: SignInWithOAuthCredentials['provider'];
+}) {
+ switch (props.provider) {
+ case 'spotify':
+ return ;
+
+ case 'github':
+ return ;
+
+ default:
+ return ;
+ }
+}
+
export default function Page(props: Props) {
return (
@@ -16,20 +51,26 @@ export default function Page(props: Props) {
A more efficient way to listen podcasts.
-
+
+ {ENABLED_OAUTH_PROVIDERS.map((provider) => (
+
+ ))}
+
By clicking continue, you acknowledge that you have read and understood,
diff --git a/components/episode-ai-summary/episode-ai-summary-footer.tsx b/components/episode-ai-summary/episode-ai-summary-footer.tsx
index 7520b15..45f7526 100644
--- a/components/episode-ai-summary/episode-ai-summary-footer.tsx
+++ b/components/episode-ai-summary/episode-ai-summary-footer.tsx
@@ -20,7 +20,7 @@ const fetchData = async (id: Props['id']) => {
const { data } = await supabase
.from('episode_content')
- .select('id, user:account(id, display_name, avatar_url)')
+ .select('id, user:account(id, name, avatar_url)')
.eq('episode', id)
.single();
@@ -59,7 +59,7 @@ export function EpisodeAISummaryFooter(props: Props) {
/>
- Generated by {data.user.display_name}
+ Generated by {data.user.name}
) : (
diff --git a/components/layout/app-header.stories.tsx b/components/layout/app-header.stories.tsx
index 6122b60..5feab27 100644
--- a/components/layout/app-header.stories.tsx
+++ b/components/layout/app-header.stories.tsx
@@ -11,7 +11,7 @@ export const Authenticated: StoryObj = {
diff --git a/components/layout/app-header.tsx b/components/layout/app-header.tsx
index 56b7397..916567d 100644
--- a/components/layout/app-header.tsx
+++ b/components/layout/app-header.tsx
@@ -29,7 +29,7 @@ type Props =
user: {
avatarURL?: string;
credits: number;
- username: string;
+ name: string;
};
variant: 'authenticated';
}
@@ -38,16 +38,16 @@ type Props =
};
const USER_MENU_LINKS = [
- {
- href: '/credits',
- icon: ,
- label: 'Buy credits',
- },
{
href: '/shows',
icon: ,
label: 'Your shows',
},
+ {
+ href: '/credits',
+ icon: ,
+ label: 'Buy credits',
+ },
];
function AppHeaderActionsAuthenticated(
@@ -77,7 +77,7 @@ function AppHeaderActionsAuthenticated(
- {props.user.username}
+ {props.user.name}
diff --git a/components/layout/app-layout.tsx b/components/layout/app-layout.tsx
index 7879a06..be354b6 100644
--- a/components/layout/app-layout.tsx
+++ b/components/layout/app-layout.tsx
@@ -19,7 +19,7 @@ export async function AppLayout({ children }: PropsWithChildren) {
user={{
avatarURL: account.avatar_url ?? '',
credits: account.ai_credit,
- username: account.display_name ?? '',
+ name: account.name,
}}
variant="authenticated"
/>
diff --git a/lib/services/account.ts b/lib/services/account.ts
index 61ee0a3..6527703 100644
--- a/lib/services/account.ts
+++ b/lib/services/account.ts
@@ -1,12 +1,64 @@
'use server';
+import type { Session, User } from '@supabase/supabase-js';
+
+import { DatabaseError } from '@/lib/errors';
+import { differenceInMinutes } from 'date-fns';
import { cookies } from 'next/headers';
import { z } from 'zod';
-import { DatabaseError } from '../errors';
+import { notifySlack } from './notify/slack';
import { getUser } from './supabase/auth';
import { createSupabaseServerClient } from './supabase/server';
+type UserMetadata = {
+ avatar_url?: string;
+ name?: string;
+ preferred_username?: string;
+ provider_id?: string;
+ user_name?: string;
+};
+
+export const updateAccount = async ({
+ session,
+ user,
+}: {
+ session: Session;
+ user: User;
+}) => {
+ const metadata = user.user_metadata as UserMetadata;
+ const supabase = createSupabaseServerClient(cookies());
+
+ const { data, error } = await supabase
+ .from('account')
+ .upsert(
+ {
+ avatar_url: metadata.avatar_url || '',
+ name:
+ metadata.preferred_username || metadata.user_name || user.email || '',
+ provider_refresh_token: session.provider_refresh_token || '',
+ provider_token: session.provider_token || '',
+ user_id: user.id,
+ },
+ { onConflict: 'user_id' },
+ )
+ .select('name, created_at')
+ .single();
+
+ if (error) {
+ throw new DatabaseError(error);
+ }
+
+ const isNewAccount =
+ differenceInMinutes(new Date(), new Date(data.created_at)) <= 5;
+
+ if (isNewAccount) {
+ await notifySlack(`🐝 New sign-up for *beecast*: ${data.name}`);
+ }
+
+ return { isNewAccount };
+};
+
export const getAccountId = async () => {
const supabase = createSupabaseServerClient(cookies());
diff --git a/lib/services/show.ts b/lib/services/show.ts
index 1e6011b..563896c 100644
--- a/lib/services/show.ts
+++ b/lib/services/show.ts
@@ -101,7 +101,7 @@ const saveShowToImported = async (
const { data, error } = await supabase.from('imported_show').insert({
podcast_index_guid: podcastIndexGuid,
show: showId,
- spotify_id: spotifyId,
+ spotify_id: spotifyId || '',
});
if (error) {
diff --git a/lib/services/spotify/save-user-info.ts b/lib/services/spotify/save-user-info.ts
deleted file mode 100644
index c2000d5..0000000
--- a/lib/services/spotify/save-user-info.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import type { Session, User } from '@supabase/supabase-js';
-
-import { DatabaseError } from '@/lib/errors';
-import { differenceInMinutes } from 'date-fns';
-import { cookies } from 'next/headers';
-import { z } from 'zod';
-
-import { notifySlack } from '../notify/slack';
-import { createSupabaseServerClient } from '../supabase/server';
-
-type UserMetadata = {
- avatar_url?: string;
- full_name?: string;
- provider_id?: string;
-};
-
-const spotifyUserInfoResponseSchema = z.object({
- avatar_url: z.string().default(''),
- full_name: z.string().default(''),
- provider_id: z.string().min(1),
- provider_token: z.string().min(1),
-});
-
-export const saveUserInfo = async ({
- session,
- user,
-}: {
- session: Session;
- user: User;
-}) => {
- const providerToken = session.provider_token;
- const userMetadata: UserMetadata = user.user_metadata;
-
- const validatedResponse = spotifyUserInfoResponseSchema.safeParse({
- avatar_url: userMetadata.avatar_url,
- full_name: userMetadata.full_name,
- provider_id: userMetadata.provider_id,
- provider_token: providerToken,
- });
-
- if (!validatedResponse.success) {
- throw validatedResponse.error;
- }
-
- const userInfo = validatedResponse.data;
- const supabase = createSupabaseServerClient(cookies());
-
- const { data, error } = await supabase
- .from('account')
- .upsert(
- {
- avatar_url: userInfo.avatar_url,
- display_name: userInfo.full_name,
- provider_refresh_token: session.provider_refresh_token,
- provider_token: userInfo.provider_token,
- spotify_id: userInfo.provider_id,
- user_id: user.id,
- },
- { onConflict: 'spotify_id' },
- )
- .select('display_name, created_at')
- .single();
-
- if (error) {
- throw new DatabaseError(error);
- }
-
- const isNewAccount =
- differenceInMinutes(new Date(), new Date(data.created_at)) <= 5;
-
- if (isNewAccount) {
- await notifySlack(`🐝 New sign-up for *beecast*: ${data.display_name}`);
- }
-
- return { isNewAccount };
-};
diff --git a/lib/services/supabase/auth.ts b/lib/services/supabase/auth.ts
index 00e1f4d..ba33ee0 100644
--- a/lib/services/supabase/auth.ts
+++ b/lib/services/supabase/auth.ts
@@ -1,5 +1,6 @@
import type { Database } from '@/types/supabase/database';
import type { CookieOptions } from '@supabase/ssr';
+import type { SignInWithOAuthCredentials } from '@supabase/supabase-js';
import type { NextRequest } from 'next/server';
import { env } from '@/env.mjs';
@@ -86,3 +87,6 @@ export const getUser = async () => {
return userQuery.data.user;
};
+
+export const ENABLED_OAUTH_PROVIDERS: SignInWithOAuthCredentials['provider'][] =
+ ['github', 'spotify'];
diff --git a/middleware.ts b/middleware.ts
index 16c4816..fcf31b3 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -1,15 +1,19 @@
import type { NextRequest } from 'next/server';
-import { getSupabaseAuthSession } from '@/lib/services/supabase/auth';
+import {
+ ENABLED_OAUTH_PROVIDERS,
+ getSupabaseAuthSession,
+} from '@/lib/services/supabase/auth';
import { NextResponse } from 'next/server';
const publicRoutes = [
'/',
'/auth/callback',
- '/auth/sign-in',
'/sign-in',
'/api/webhooks/stripe',
-];
+].concat(
+ ENABLED_OAUTH_PROVIDERS.map((provider) => `/auth/sign-in/${provider}`),
+);
export async function middleware(request: NextRequest) {
const { pathname } = new URL(request.url);
diff --git a/types/supabase/database.ts b/types/supabase/database.ts
index ef31ca7..28c3b1a 100644
--- a/types/supabase/database.ts
+++ b/types/supabase/database.ts
@@ -14,47 +14,44 @@ export interface Database {
ai_credit: number;
avatar_url: string | null;
created_at: string;
- display_name: string | null;
id: number;
+ name: string;
provider_refresh_token: string | null;
provider_token: string;
- spotify_id: string;
user_id: string;
};
Insert: {
ai_credit?: number;
avatar_url?: string | null;
created_at?: string;
- display_name?: string | null;
id?: number;
+ name: string;
provider_refresh_token?: string | null;
provider_token: string;
- spotify_id: string;
user_id: string;
};
Update: {
ai_credit?: number;
avatar_url?: string | null;
created_at?: string;
- display_name?: string | null;
id?: number;
+ name?: string;
provider_refresh_token?: string | null;
provider_token?: string;
- spotify_id?: string;
user_id?: string;
};
Relationships: [
{
foreignKeyName: 'account_user_fkey';
columns: ['user_id'];
- isOneToOne: false;
+ isOneToOne: true;
referencedRelation: 'users';
referencedColumns: ['id'];
},
{
foreignKeyName: 'account_user_id_fkey';
columns: ['user_id'];
- isOneToOne: false;
+ isOneToOne: true;
referencedRelation: 'users';
referencedColumns: ['id'];
},
@@ -321,7 +318,7 @@ export interface Database {
language: string | null;
podcast_index_guid: string;
publisher: string;
- spotify_id: string;
+ spotify_id: string | null;
title: string;
total_episode: number | null;
};
@@ -333,7 +330,7 @@ export interface Database {
language?: string | null;
podcast_index_guid: string;
publisher: string;
- spotify_id: string;
+ spotify_id?: string | null;
title: string;
total_episode?: number | null;
};
@@ -345,7 +342,7 @@ export interface Database {
language?: string | null;
podcast_index_guid?: string;
publisher?: string;
- spotify_id?: string;
+ spotify_id?: string | null;
title?: string;
total_episode?: number | null;
};