Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: OAuth #35

Merged
merged 8 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,17 @@ The blog is based on the instructions in the blog post
[Building a Blog with Fresh](https://deno.com/blog/build-a-blog-with-fresh). For
more information about how the blog works, see the reference post.

### OAuth

1. Set up the OAuth application for your given provider by following one of
[these guides](https://supabase.com/docs/guides/auth#providers).
1. [Configure your third-party provider](https://supabase.com/docs/guides/auth#configure-third-party-providers)
in Supabase.
1. Insert your `OAuthLoginButton` component in your login or signup page
[as follows](routes/login.tsx), which has been done for GitHub. Note: you may
need to create the provider icon, which can be done in
[components/Icons.tsx](components/Icons.tsx).

### Theme Customization

You can customize theme options such as spacing, color, etc. By default, Deno
Expand Down
26 changes: 26 additions & 0 deletions components/Icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copied from https://github.com/denoland/dotland/blob/main/components/Icons.tsx
export function GitHub(props: { class?: string }) {
return (
<svg
class={props.class}
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="GitHub Logo"
>
<g clip-path="url(#clip0_1989_191)">
<path
d="M7.00001 0C3.13391 0 0 3.21295 0 7.17755C0 10.3482 2.0055 13.0388 4.7873 13.9875C5.1373 14.0534 5.26471 13.832 5.26471 13.6414C5.26471 13.4716 5.25912 13.0195 5.25561 12.4212C3.3082 12.8547 2.8973 11.4589 2.8973 11.4589C2.5795 10.6291 2.1203 10.4084 2.1203 10.4084C1.48471 9.96418 2.16861 9.97279 2.16861 9.97279C2.87071 10.0229 3.24032 10.7122 3.24032 10.7122C3.86472 11.8085 4.87903 11.4918 5.27732 11.3084C5.34171 10.8448 5.52232 10.5288 5.72251 10.3497C4.16851 10.1684 2.534 9.55218 2.534 6.80211C2.534 6.01893 2.807 5.37764 3.2543 4.87605C3.1822 4.69476 2.94211 3.96463 3.32289 2.97722C3.32289 2.97722 3.91089 2.78376 5.24789 3.71238C5.77305 3.55992 6.37629 3.47184 6.99948 3.4709C7.59448 3.47377 8.19351 3.5533 8.7528 3.71238C10.0891 2.78376 10.6757 2.97649 10.6757 2.97649C11.0579 3.9646 10.8171 4.69475 10.7457 4.87603C11.1937 5.3776 11.4653 6.0189 11.4653 6.80208C11.4653 9.55931 9.82799 10.1662 8.26908 10.3439C8.52037 10.5653 8.74368 11.0031 8.74368 11.6731C8.74368 12.6318 8.73529 13.4064 8.73529 13.6414C8.73529 13.8335 8.86129 14.057 9.21689 13.9868C12.0205 13.0032 14 10.3285 14 7.18046C14 7.17943 14 7.17841 14 7.17738C14 3.21278 10.8654 0 7.00001 0Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_1989_191">
<rect width="14" height="14" fill="white" />
</clipPath>
</defs>
</svg>
);
}
27 changes: 27 additions & 0 deletions components/OAuthLoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Button from "@/components/Button.tsx";
import type { Provider } from "@supabase/supabase-js";
import type { GitHub } from "./Icons.tsx";

function capitalize(value: string) {
return value[0].toUpperCase() + value.slice(1);
}

interface OAuthLoginButtonProps {
provider: Provider;
icon: typeof GitHub;
}

export default function OAuthLoginButton(props: OAuthLoginButtonProps) {
return (
<form action="/api/oauth" method="POST">
<input type="hidden" value={props.provider} name="provider" />
<Button
type="submit"
class="w-full bg-white! text-black border-black border-2 align-middle"
>
<props.icon class="inline mr-2" />Log in with{" "}
{capitalize(props.provider)}
</Button>
</form>
);
}
70 changes: 38 additions & 32 deletions fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,53 @@ import * as $0 from "./routes/_404.tsx";
import * as $1 from "./routes/_500.tsx";
import * as $2 from "./routes/api/login.ts";
import * as $3 from "./routes/api/logout.ts";
import * as $4 from "./routes/api/signup.ts";
import * as $5 from "./routes/api/subscription.ts";
import * as $6 from "./routes/blog/[slug].tsx";
import * as $7 from "./routes/blog/index.tsx";
import * as $8 from "./routes/dashboard/_middleware.ts";
import * as $9 from "./routes/dashboard/account.tsx";
import * as $10 from "./routes/dashboard/api/todo.ts";
import * as $11 from "./routes/dashboard/index.tsx";
import * as $12 from "./routes/dashboard/manage-subscription.ts";
import * as $13 from "./routes/dashboard/todos.tsx";
import * as $14 from "./routes/dashboard/upgrade-subscription.ts";
import * as $15 from "./routes/index.tsx";
import * as $16 from "./routes/login.tsx";
import * as $17 from "./routes/logout.ts";
import * as $18 from "./routes/signup.tsx";
import * as $$0 from "./islands/TodoList.tsx";
import * as $4 from "./routes/api/oauth.ts";
import * as $5 from "./routes/api/signup.ts";
import * as $6 from "./routes/api/subscription.ts";
import * as $7 from "./routes/blog/[slug].tsx";
import * as $8 from "./routes/blog/index.tsx";
import * as $9 from "./routes/dashboard/_middleware.ts";
import * as $10 from "./routes/dashboard/account.tsx";
import * as $11 from "./routes/dashboard/api/todo.ts";
import * as $12 from "./routes/dashboard/index.tsx";
import * as $13 from "./routes/dashboard/manage-subscription.ts";
import * as $14 from "./routes/dashboard/todos.tsx";
import * as $15 from "./routes/dashboard/upgrade-subscription.ts";
import * as $16 from "./routes/index.tsx";
import * as $17 from "./routes/login-success.tsx";
import * as $18 from "./routes/login.tsx";
import * as $19 from "./routes/logout.ts";
import * as $20 from "./routes/signup.tsx";
import * as $$0 from "./islands/AuthFragmentCatcher.tsx";
import * as $$1 from "./islands/TodoList.tsx";

const manifest = {
routes: {
"./routes/_404.tsx": $0,
"./routes/_500.tsx": $1,
"./routes/api/login.ts": $2,
"./routes/api/logout.ts": $3,
"./routes/api/signup.ts": $4,
"./routes/api/subscription.ts": $5,
"./routes/blog/[slug].tsx": $6,
"./routes/blog/index.tsx": $7,
"./routes/dashboard/_middleware.ts": $8,
"./routes/dashboard/account.tsx": $9,
"./routes/dashboard/api/todo.ts": $10,
"./routes/dashboard/index.tsx": $11,
"./routes/dashboard/manage-subscription.ts": $12,
"./routes/dashboard/todos.tsx": $13,
"./routes/dashboard/upgrade-subscription.ts": $14,
"./routes/index.tsx": $15,
"./routes/login.tsx": $16,
"./routes/logout.ts": $17,
"./routes/signup.tsx": $18,
"./routes/api/oauth.ts": $4,
"./routes/api/signup.ts": $5,
"./routes/api/subscription.ts": $6,
"./routes/blog/[slug].tsx": $7,
"./routes/blog/index.tsx": $8,
"./routes/dashboard/_middleware.ts": $9,
"./routes/dashboard/account.tsx": $10,
"./routes/dashboard/api/todo.ts": $11,
"./routes/dashboard/index.tsx": $12,
"./routes/dashboard/manage-subscription.ts": $13,
"./routes/dashboard/todos.tsx": $14,
"./routes/dashboard/upgrade-subscription.ts": $15,
"./routes/index.tsx": $16,
"./routes/login-success.tsx": $17,
"./routes/login.tsx": $18,
"./routes/logout.ts": $19,
"./routes/signup.tsx": $20,
},
islands: {
"./islands/TodoList.tsx": $$0,
"./islands/AuthFragmentCatcher.tsx": $$0,
"./islands/TodoList.tsx": $$1,
},
baseUrl: import.meta.url,
config,
Expand Down
2 changes: 1 addition & 1 deletion import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"std/": "https://deno.land/std@0.181.0/",
"stripe": "https://esm.sh/stripe@11.13.0",
"tabler-icons/": "https://deno.land/x/tabler_icons_tsx@0.0.2/tsx/",
"@supabase/auth-helpers-shared": "https://esm.sh/@supabase/auth-helpers-shared@0.3.0",
"@supabase/auth-helpers-shared": "https://esm.sh/@supabase/auth-helpers-shared@0.3.0?target=es2022",
"@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.12.1"
}
}
24 changes: 24 additions & 0 deletions islands/AuthFragmentCatcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect } from "preact/hooks";
import { createBrowserSupabaseClient } from "@supabase/auth-helpers-shared";
import { AUTHENTICATED_REDIRECT_PATH } from "@/constants.ts";

export default function AuthFragmentCatcher(
props: Parameters<typeof createBrowserSupabaseClient>[0],
) {
useEffect(() => {
const supabase = createBrowserSupabaseClient(props);
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
if (session?.user) {
window.location.href = AUTHENTICATED_REDIRECT_PATH;
}
});

return () => {
subscription.unsubscribe();
};
}, []);

return <span></span>;
}
29 changes: 29 additions & 0 deletions routes/api/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Handlers } from "$fresh/server.ts";
import { assert } from "std/testing/asserts.ts";
import { createSupabaseClient } from "@/utils/supabase.ts";
import type { Provider } from "@supabase/supabase-js";

export const handler: Handlers = {
async POST(request) {
const form = await request.formData();
const provider = form.get("provider");

assert(typeof provider === "string");
iuioiua marked this conversation as resolved.
Show resolved Hide resolved

const headers = new Headers();
const supabaseClient = createSupabaseClient(request.headers, headers);
const { data, error } = await supabaseClient.auth.signInWithOAuth({
provider: provider as Provider,
options: {
redirectTo: new URL(request.url).origin +
"/login-success",
},
});

if (error) throw error;

headers.set("location", data.url);

return new Response(null, { headers, status: 302 });
},
};
10 changes: 10 additions & 0 deletions routes/login-success.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import AuthFragmentCatcher from "@/islands/AuthFragmentCatcher.tsx";

export default function OAuthSuccessPage() {
return (
<AuthFragmentCatcher
supabaseUrl={Deno.env.get("SUPABASE_URL")!}
supabaseKey={Deno.env.get("SUPABASE_ANON_KEY")!}
/>
);
}
3 changes: 3 additions & 0 deletions routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Logo from "@/components/Logo.tsx";
import Head from "@/components/Head.tsx";
import AuthForm from "@/components/AuthForm.tsx";
import Notice from "@/components/Notice.tsx";
import OAuthLoginButton from "@/components/OAuthLoginButton.tsx";
import * as Icons from "@/components/Icons.tsx";

export default function LoginPage(props: PageProps) {
const errorMessage = props.url.searchParams.get("error");
Expand All @@ -18,6 +20,7 @@ export default function LoginPage(props: PageProps) {
{errorMessage === "Invalid login credentials" && (
<Notice>{errorMessage}</Notice>
)}
<OAuthLoginButton provider="github" icon={Icons.GitHub} />
<AuthForm type="Login" />
<div class="text-center text-gray-500 hover:text-black">
<a href="/signup">Don't have an account? Sign up</a>
Expand Down
3 changes: 3 additions & 0 deletions routes/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Head from "@/components/Head.tsx";
import AuthForm from "@/components/AuthForm.tsx";
import Notice from "@/components/Notice.tsx";
import Logo from "@/components/Logo.tsx";
import OAuthLoginButton from "@/components/OAuthLoginButton.tsx";
import * as Icons from "@/components/Icons.tsx";

export default function SignupPage(props: PageProps) {
const errorMessage = props.url.searchParams.get("error");
Expand All @@ -18,6 +20,7 @@ export default function SignupPage(props: PageProps) {
{errorMessage === "User already registered" && (
<Notice>{errorMessage}</Notice>
)}
<OAuthLoginButton provider="github" icon={Icons.GitHub} />
iuioiua marked this conversation as resolved.
Show resolved Hide resolved
<AuthForm type="Signup" />
<div class="text-center text-gray-500 hover:text-black">
<a href="/login">Already have an account? Log in</a>
Expand Down