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

Supabase SSR #135

Merged
merged 10 commits into from
Aug 30, 2024
219 changes: 144 additions & 75 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
},
"type": "module",
"dependencies": {
"@supabase/auth-helpers-sveltekit": "^0.11.0",
"@supabase/auth-ui-svelte": "^0.2.9",
"@supabase/supabase-js": "^2.33.0",
"@supabase/ssr": "^0.5.1",
"@supabase/supabase-js": "^2.45.2",
"resend": "^3.5.0",
"stripe": "^13.3.0"
}
Expand Down
8 changes: 6 additions & 2 deletions src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SupabaseClient, Session } from "@supabase/supabase-js"
import { Session, SupabaseClient, type AMREntry } from "@supabase/supabase-js"
import { Database } from "./DatabaseDefinitions"

// See https://kit.svelte.dev/docs/types#app
Expand All @@ -8,7 +8,11 @@ declare global {
interface Locals {
supabase: SupabaseClient<Database>
supabaseServiceRole: SupabaseClient<Database>
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
safeGetSession: () => Promise<{
session: Session | null
user: User | null
amr: AMREntry[] | null
}>
}
interface PageData {
session: Session | null
Expand Down
58 changes: 44 additions & 14 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
// src/hooks.server.ts
import { PRIVATE_SUPABASE_SERVICE_ROLE } from "$env/static/private"
import {
PUBLIC_SUPABASE_URL,
PUBLIC_SUPABASE_ANON_KEY,
PUBLIC_SUPABASE_URL,
} from "$env/static/public"
import { PRIVATE_SUPABASE_SERVICE_ROLE } from "$env/static/private"
import { createSupabaseServerClient } from "@supabase/auth-helpers-sveltekit"
import { createServerClient } from "@supabase/ssr"
import { createClient } from "@supabase/supabase-js"
import type { Handle } from "@sveltejs/kit"

export const handle: Handle = async ({ event, resolve }) => {
event.locals.supabase = createSupabaseServerClient({
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event,
})
event.locals.supabase = createServerClient(
PUBLIC_SUPABASE_URL,
PUBLIC_SUPABASE_ANON_KEY,
{
cookies: {
getAll: () => event.cookies.getAll(),
/**
* SvelteKit's cookies API requires `path` to be explicitly set in
* the cookie options. Setting `path` to `/` replicates previous/
* standard behavior.
*/
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
event.cookies.set(name, value, { ...options, path: "/" })
})
},
},
},
)

event.locals.supabaseServiceRole = createClient(
PUBLIC_SUPABASE_URL,
PRIVATE_SUPABASE_SERVICE_ROLE,
{ auth: { persistSession: false } },
)

// https://github.com/supabase/auth-js/issues/888#issuecomment-2189298518
if ("suppressGetSessionWarning" in event.locals.supabase.auth) {
// @ts-expect-error - suppressGetSessionWarning is not part of the official API
event.locals.supabase.auth.suppressGetSessionWarning = true
} else {
console.warn(
"SupabaseAuthClient#suppressGetSessionWarning was removed. See https://github.com/supabase/auth-js/issues/888.",
)
}

/**
* Unlike `supabase.auth.getSession()`, which returns the session _without_
* validating the JWT, this function also calls `getUser()` to validate the
Expand All @@ -31,24 +55,30 @@ export const handle: Handle = async ({ event, resolve }) => {
data: { session },
} = await event.locals.supabase.auth.getSession()
if (!session) {
return { session: null, user: null }
return { session: null, user: null, amr: null }
}

const {
data: { user },
error,
error: userError,
} = await event.locals.supabase.auth.getUser()
if (error) {
if (userError) {
// JWT validation has failed
return { session: null, user: null }
return { session: null, user: null, amr: null }
}

const { data: aal, error: amrError } =
await event.locals.supabase.auth.mfa.getAuthenticatorAssuranceLevel()
if (amrError) {
return { session, user, amr: null }
}

return { session, user }
return { session, user, amr: aal.currentAuthenticationMethods }
}

return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === "content-range"
return name === "content-range" || name === "x-supabase-api-version"
},
})
}
10 changes: 5 additions & 5 deletions src/routes/(admin)/account/(menu)/billing/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { redirect, error } from "@sveltejs/kit"
import { error, redirect } from "@sveltejs/kit"
import {
getOrCreateCustomerId,
fetchSubscription,
getOrCreateCustomerId,
} from "../../subscription_helpers.server"
import type { PageServerLoad } from "./$types"

export const load: PageServerLoad = async ({
locals: { safeGetSession, supabaseServiceRole },
}) => {
const { session } = await safeGetSession()
if (!session) {
const { session, user } = await safeGetSession()
if (!session || !user?.id) {
redirect(303, "/login")
}

const { error: idError, customerId } = await getOrCreateCustomerId({
supabaseServiceRole,
session,
user,
})
if (idError || !customerId) {
error(500, {
Expand Down
10 changes: 5 additions & 5 deletions src/routes/(admin)/account/(menu)/billing/manage/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { redirect, error } from "@sveltejs/kit"
import { getOrCreateCustomerId } from "../../../subscription_helpers.server"
import type { PageServerLoad } from "./$types"
import { PRIVATE_STRIPE_API_KEY } from "$env/static/private"
import { error, redirect } from "@sveltejs/kit"
import Stripe from "stripe"
import { getOrCreateCustomerId } from "../../../subscription_helpers.server"
import type { PageServerLoad } from "./$types"
const stripe = new Stripe(PRIVATE_STRIPE_API_KEY, { apiVersion: "2023-08-16" })

export const load: PageServerLoad = async ({
url,
locals: { safeGetSession, supabaseServiceRole },
}) => {
const { session } = await safeGetSession()
const { session, user } = await safeGetSession()
if (!session) {
redirect(303, "/login")
}

const { error: idError, customerId } = await getOrCreateCustomerId({
supabaseServiceRole,
session,
user,
})
if (idError || !customerId) {
error(500, {
Expand Down
4 changes: 2 additions & 2 deletions src/routes/(admin)/account/(menu)/settings/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
adminSection.set("settings")

export let data
let { session, profile } = data
let { profile, user } = data
</script>

<svelte:head>
Expand Down Expand Up @@ -39,7 +39,7 @@
<SettingsModule
title="Email"
editable={false}
fields={[{ id: "email", initialValue: session?.user?.email || "" }]}
fields={[{ id: "email", initialValue: user?.email || "" }]}
editButtonTitle="Change Email"
editLink="/account/settings/change_email"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script lang="ts">
import SettingsModule from "../settings_module.svelte"
import { getContext } from "svelte"
import type { Writable } from "svelte/store"
import SettingsModule from "../settings_module.svelte"

let adminSection: Writable<string> = getContext("adminSection")
adminSection.set("settings")

export let data

let { session } = data
let { user } = data
scosman marked this conversation as resolved.
Show resolved Hide resolved
</script>

<svelte:head>
Expand All @@ -27,7 +27,7 @@
{
id: "email",
label: "Email",
initialValue: session?.user?.email ?? "",
initialValue: user?.email ?? "",
placeholder: "Email address",
},
]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,25 @@
adminSection.set("settings")

export let data
let { session, supabase } = data
let { user, supabase } = data

// True if definitely has a password, but can be false if they
// logged in with oAuth or email link

// @ts-expect-error: we ignore because Supabase does not maintain an AMR typedef
let hasPassword = session?.user?.amr?.find((x) => x.method === "password")
let hasPassword = user?.amr?.find((x) => x.method === "password")
? true
: false

// @ts-expect-error: we ignore because Supabase does not maintain an AMR typedef
let usingOAuth = session?.user?.amr?.find((x) => x.method === "oauth")
? true
: false
let usingOAuth = user?.amr?.find((x) => x.method === "oauth") ? true : false

let sendBtn: HTMLButtonElement
let sentEmail = false
let sendForgotPassword = () => {
sendBtn.disabled = true
sendBtn.textContent = "Sending..."
let email = session?.user.email
let email = user?.email
if (email) {
supabase.auth
.resetPasswordForEmail(email, {
Expand Down Expand Up @@ -91,8 +89,8 @@
<div class="font-bold">Change Password By Email</div>
{/if}
<div>
The button below will send you an email at {session?.user?.email} which will
allow you to set your password.
The button below will send you an email at {user?.email} which will allow
you to set your password.
</div>
<button
class="btn btn-outline btn-wide {sentEmail ? 'hidden' : ''}"
Expand Down
9 changes: 5 additions & 4 deletions src/routes/(admin)/account/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ import type { LayoutServerLoad } from "./$types"

export const load: LayoutServerLoad = async ({
locals: { supabase, safeGetSession },
cookies,
}) => {
const { session } = await safeGetSession()
const { session, user } = await safeGetSession()

if (!session) {
if (!session || !user?.id) {
redirect(303, "/login")
}

const { data: profile } = await supabase
.from("profiles")
.select(`*`)
.eq("id", session.user.id)
.eq("id", user?.id)
.single()

return { session, profile }
return { session, profile, cookies: cookies.getAll() }
}
59 changes: 50 additions & 9 deletions src/routes/(admin)/account/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,60 @@ import {
PUBLIC_SUPABASE_ANON_KEY,
PUBLIC_SUPABASE_URL,
} from "$env/static/public"
import { createSupabaseLoadClient } from "@supabase/auth-helpers-sveltekit"
import type { Database } from "../../../DatabaseDefinitions.js"
import {
createBrowserClient,
createServerClient,
isBrowser,
} from "@supabase/ssr"
import { redirect } from "@sveltejs/kit"
import type { Database } from "../../../DatabaseDefinitions.js"
import { CreateProfileStep } from "../../../config"

export const load = async ({ fetch, data, depends, url }) => {
depends("supabase:auth")

const supabase = createSupabaseLoadClient({
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event: { fetch },
serverSession: data.session,
})
const supabase = isBrowser()
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch,
},
})
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch,
},
cookies: {
getAll() {
return data.cookies
},
},
})

/**
* It's fine to use `getSession` here, because on the client, `getSession` is
* safe, and on the server, it reads `session` from the `LayoutData`, which
* safely checked the session using `safeGetSession`.
* Source: https://supabase.com/docs/guides/auth/server-side/sveltekit
*/
const {
data: { session },
} = await supabase.auth.getSession()

// https://github.com/supabase/auth-js/issues/888#issuecomment-2189298518
if ("suppressGetSessionWarning" in supabase.auth) {
// @ts-expect-error - suppressGetSessionWarning is not part of the official API
supabase.auth.suppressGetSessionWarning = true
} else {
console.warn(
"SupabaseAuthClient#suppressGetSessionWarning was removed. See https://github.com/supabase/auth-js/issues/888.",
)
}
const {
data: { user },
} = await supabase.auth.getUser()

const { data: aal } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()

const profile: Database["public"]["Tables"]["profiles"]["Row"] | null =
data.profile

Expand All @@ -36,7 +71,13 @@ export const load = async ({ fetch, data, depends, url }) => {
redirect(303, createProfilePath)
}

return { supabase, session, profile }
return {
supabase,
session,
profile,
user,
amr: aal?.currentAuthenticationMethods,
}
}

export const _hasFullProfile = (
Expand Down
Loading
Loading