Reputation: 2766
In a Supabase app, I want to invite users (instead of them signing themselves up). I can invite a user with their email, but they get sent a link which directly authenticates them (like a magic link).
I rather have the user set their password the first time they get into the app. This way, the user would be able to log out and log back in again.
So what I'm looking for is more like a regular signup with email verification, only the order switched around: First you get an email, then you set your password.
Is that even possible with Supabase? If so, how? Or is this just old-fashioned thinking of me and should I go for the way the email invite is set up by Supabase?
Upvotes: 6
Views: 6815
Reputation: 201
I got this combination to work with the new @supabase/ssr library in Next.js 13 using the supabase token_hash.
Dont forget to update the supabase email templates.
/util/supabase/server.ts
import { Database } from '@/types/database'
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export const createClient = (cookieStore: ReturnType<typeof cookies>) => {
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
db: {
schema: 'public'
},
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options })
} catch (error) {
// The `delete` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
app/auth/confirm/route.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { type EmailOtpType } from '@supabase/supabase-js'
import { createClient } from '@/utils/supabase/server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type') as EmailOtpType | null
const next = searchParams.get('next') ?? '/'
if (token_hash && type) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
if (!error) {
return redirect(next)
}
}
// return the user to an error page with some instructions
return redirect('/auth/auth-code-error')
}
app/set-password/page.tsx
import Link from 'next/link'
import { cookies } from 'next/headers'
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'
import { supportEmail } from '@/utils/constants'
export default async function SetPassword({
searchParams,
}: {
searchParams: { message: string }
}) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { data: { user }, } = await supabase.auth.getUser()
const setPassword = async (formData: FormData) => {
'use server'
const email = user?.email
const password = formData.get('password') as string
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { error } = await supabase.auth.updateUser({
password,
})
if (error) {
return redirect('/set-password?message=Could not update user password')
}
return redirect('/')
}
return (
<div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2">
<Link
href="/"
className="absolute left-8 top-4 py-2 px-4 rounded-md no-underline text-foreground bg-btn-background hover:bg-btn-background-hover flex items-center group text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1"
>
<polyline points="15 18 9 12 15 6" />
</svg>{' '}
Back
</Link>
{user ?
<form
className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground"
action={setPassword}
>
<label className="text-md" htmlFor="email">
Email
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="email"
value={user?.email}
disabled
/>
<label className="text-md" htmlFor="password">
Password
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
type="password"
name="password"
placeholder="••••••••"
required
/>
<button
type="submit"
className="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Set Password
</button>
{searchParams?.message && (
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
{searchParams.message}
</p>
)}
</form>
:
<div className="mx-auto max-w-2xl text-center flex flex-col space-y-4">
<p className="mt-2 text-lg leading-8 text-gray-600">
You must be logged in to use this application.
</p>
<p className="mt-2 text-lg leading-8 text-gray-600">
If you do not have an account, contact <a href={`mailto:${supportEmail}`} className='text-indigo-600'>{supportEmail}</a>.
</p>
</div>
}
</div>
)
}
Upvotes: 1
Reputation: 2766
I just found an answer on github discussions, where the same question was discussed at the same time.
Since the user is already authed, you can get update the user with their new password:
const { data, error } = await supabase.auth.update({ password: "password" });
Leaving it here for future reference / others who may have the same question. More details on the github thread below:
https://github.com/supabase/supabase/discussions/3208
Upvotes: 12