Skip to content

Commit

Permalink
feat: add login with github (#97)
Browse files Browse the repository at this point in the history
* feat: add login with github

* fix: types
  • Loading branch information
altaywtf authored Dec 17, 2023
1 parent 94dd82c commit 6dace10
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 137 deletions.
19 changes: 5 additions & 14 deletions app/(auth)/auth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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({
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
import type { SignInWithOAuthCredentials } from '@supabase/supabase-js';
import type { NextRequest } from 'next/server';

import { HttpAuthenticationError } from '@/lib/errors';
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) {
Expand Down
69 changes: 55 additions & 14 deletions app/(auth)/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
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?: {
redirect?: string;
};
};

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 <FaSpotify />;

case 'github':
return <FaGithub />;

default:
return <CgLogIn />;
}
}

export default function Page(props: Props) {
return (
<Flex direction="column" gap="4">
Expand All @@ -16,20 +51,26 @@ export default function Page(props: Props) {
A more efficient way to listen podcasts.
</Text>

<form action="/auth/sign-in" method="POST">
<input
name="redirect"
type="hidden"
value={props.searchParams?.redirect}
/>

<Flex direction="column">
<Button highContrast size="3" type="submit">
<FaSpotify />
Continue with Spotify
</Button>
</Flex>
</form>
<Flex direction="column" gap="2">
{ENABLED_OAUTH_PROVIDERS.map((provider) => (
<div key={provider}>
<form action={`/auth/sign-in/${provider}`} method="POST">
<input
name="redirect"
type="hidden"
value={props.searchParams?.redirect}
/>

<Flex direction="column">
<Button highContrast size="3" type="submit">
<AuthProviderIcon provider={provider} />
Continue with <AuthProviderLabel provider={provider} />
</Button>
</Flex>
</form>
</div>
))}
</Flex>

<Text color="gray" size="1" trim="both">
By clicking continue, you acknowledge that you have read and understood,
Expand Down
4 changes: 2 additions & 2 deletions components/episode-ai-summary/episode-ai-summary-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -59,7 +59,7 @@ export function EpisodeAISummaryFooter(props: Props) {
/>

<Text color="gray" size="1">
Generated by <Text highContrast>{data.user.display_name}</Text>
Generated by <Text highContrast>{data.user.name}</Text>
</Text>
</Flex>
) : (
Expand Down
2 changes: 1 addition & 1 deletion components/layout/app-header.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const Authenticated: StoryObj<typeof AppHeader> = {
<AppHeader
user={{
credits: 10,
username: 'Mr. Bee',
name: 'Mr. Bee',
}}
variant="authenticated"
/>
Expand Down
14 changes: 7 additions & 7 deletions components/layout/app-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Props =
user: {
avatarURL?: string;
credits: number;
username: string;
name: string;
};
variant: 'authenticated';
}
Expand All @@ -38,16 +38,16 @@ type Props =
};

const USER_MENU_LINKS = [
{
href: '/credits',
icon: <PiRobotBold />,
label: 'Buy credits',
},
{
href: '/shows',
icon: <CgMediaPodcast />,
label: 'Your shows',
},
{
href: '/credits',
icon: <PiRobotBold />,
label: 'Buy credits',
},
];

function AppHeaderActionsAuthenticated(
Expand Down Expand Up @@ -77,7 +77,7 @@ function AppHeaderActionsAuthenticated(
<DropdownMenuLabel asChild>
<Flex align="start" direction="column" height="auto">
<Text color="gray" highContrast weight="medium">
{props.user.username}
{props.user.name}
</Text>

<Text color="gray" size="1">
Expand Down
2 changes: 1 addition & 1 deletion components/layout/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
Expand Down
54 changes: 53 additions & 1 deletion lib/services/account.ts
Original file line number Diff line number Diff line change
@@ -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());

Expand Down
2 changes: 1 addition & 1 deletion lib/services/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

1 comment on commit 6dace10

@vercel
Copy link

@vercel vercel bot commented on 6dace10 Dec 17, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.