Supabase set password after email invite
Asked Answered
H

2

6

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?

Heliacal answered 15/9, 2021 at 21:21 Comment(0)
H
12

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

Heliacal answered 16/9, 2021 at 14:37 Comment(4)
Is there any way to handle these cases in mobile app wirh only REST endpoints?Samara
This is all using rest. Curl commands are being displayed next to the javascript commands in the docs.Heliacal
how is the user authed? For me link has the form domain/#access_token=...&expires_in=3600&refresh_token=...&token_type=bearer&type=invite, and any supabase auth methods return 'no session' error.Clarke
@Clarke did you ever solve this issue?Gothenburg
E
1

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>
  )
}
Emden answered 16/11, 2023 at 23:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.