0

I am working on a SvelteKit application with a Supabase back-end. I just finished on input validation, and now I am trying to display errors for invalid credentials. I am struggling find a way to do this.

Here is src/routes/login/+page.svelte:

<script>
  import { superForm } from "sveltekit-superforms";

  export let data;

  let showAuthError = false;

  const { form, errors, enhance } = superForm(data.form);

  let selectedLoginMethod = "email"

</script>

  <main class="flex justify-center items-center h-screen">
    <div class="w-full max-w-md">
      <h1 class="text-center text-3xl font-bold mb-4">Login</h1>

      <div class="button-container justify-center items-center flex mb-4">
        <button 
          type="button" 
          class={`btn btn-primary ${selectedLoginMethod === "email" ? "active" : ""}`}
          on:click={() => (selectedLoginMethod = "email")}
        >
          Email
        </button>
        <button 
          type="button" 
          class={`btn btn-primary ${selectedLoginMethod === "emailCode" ? "active" : ""}`}
          on:click={() => (selectedLoginMethod = "emailCode")}
        >
          Email code
        </button>
        <button 
          type="button" 
          class={`btn btn-primary ${selectedLoginMethod === "google" ? "active" : ""}`}
          on:click={() => (selectedLoginMethod = "google")}
        >
          Google
        </button>
      </div>

      {#if selectedLoginMethod === "email"}
        <form method="POST" class="space-y-4" use:enhance>
          {#if $errors.email}
            <div role="alert" class="alert alert-error">
                <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                <span>{$errors.email}</span>
            </div>
          {/if}
          {#if $errors.password}
            <div role="alert" class="alert alert-error">
                <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                <span>{$errors.password}</span>
            </div>
          {/if}
          {#if showAuthError}
          <div role="alert" class="alert alert-error">
            <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
            <span>Invalid Credentials</span>
          </div>
          {/if}
          <input type="hidden" name="loginMethod" bind:value={selectedLoginMethod} />
          <input type="email" placeholder="[email protected]" name="email" aria-invalid={$errors.email ? 'true' : undefined} bind:value={$form.email} class="input input-bordered input-primary w-full max-w-xl" />
          <input type="password" placeholder="Pa$$word123!" name="password" aria-invalid={$errors.password ? 'true' : undefined} bind:value={$form.password} class="input input-bordered input-primary w-full max-w-xl" />
          <a class="block mb-2 text-secondary hover:text-primary" href="/signup">New to the platform? Join now</a>
          <button type="submit" class="btn btn-primary grow">
            Sign in
          </button>
        </form>
      {:else if selectedLoginMethod === "emailCode"}
        <form method="POST" class="space-y-4" use:enhance>
          {#if $errors.email}
            <div role="alert" class="alert alert-error">
                <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                <span>{$errors.email}</span>
            </div>
          {/if}
          <input type="hidden" name="loginMethod" bind:value={selectedLoginMethod} />
          <input type="email" placeholder="[email protected]" name="email" bind:value={$form.email} required class="input input-bordered input-primary w-full max-w-xl" />
          <button type="submit" class="btn btn-primary grow">
            {selectedLoginMethod === "magicLink" ? "Send link" : "Send code"}
          </button>
        </form>
      {:else if selectedLoginMethod === "google"}
        <form method="POST" action="/login" class="space-y-4" use:enhance>
          <input type="hidden" name="loginMethod" value="google" />
          <button type="submit" class="btn btn-primary grow">
            Sign in with Google
          </button>
        </form>
      {/if}

    </div>
  </main>

  <style>
    .button-container {
      display: flex;
      gap: 1rem;
    }
  </style>

And here is src/routes/login/+page.server.js:

import { superValidate } from "sveltekit-superforms";
import { joi } from "sveltekit-superforms/adapters";
import Joi from "joi"
import { fail, redirect } from "@sveltejs/kit";
import { AuthApiError } from "@supabase/supabase-js";

const defaultLoginSchema = Joi.object({
    email: Joi.string()
        .required()
        .email({ minDomainSegments: 2 })
        .messages({
            "string.email": "Enter a valid email address",
            "string.empty": "Email is required"
        }),
    password: Joi.string()
        .required()
        .messages({"string.empty": "Password is required"}),
    loginMethod: Joi.string()
        .default('email')
});

const passwordlessLoginSchema = Joi.object({
    email: Joi.string()
        .required()
        .email({ minDomainSegments: 2 })
        .messages({
            "string.email": "Enter a valid email address",
            "string.empty": "Email is required"
        }),
    loginMethod: Joi.string()
        .default('emailCode')
});

/** @type {import('./$types').PageServerLoad} */
export const load = (async () => {
    const form = await superValidate(joi(defaultLoginSchema));

    return { form };
});

export const actions = {
    async default({ request, locals }) {
        const formData = await request.formData();
        const selectedLoginMethod = formData.get("loginMethod");

        if (selectedLoginMethod === "email") {

            const form = await superValidate(formData, joi(defaultLoginSchema))

            if (!form.valid) {
                console.error("Validaton Error:", form);
                return fail(400, { form });
            }

            const { data, error } = await locals.supabase.auth.signInWithPassword({
                email: form.data.email,
                password: form.data.password,
            });

            if (error) {
                if (error instanceof AuthApiError && error.status === 400) {
                    console.error("Invalid credentials", error)
                    return fail(400, {
                        error: "Invalid credentials",
                        email: form.data.email,
                        invalid: true,
                        message: error.message,
                        form: form,
                        authApiError: true
                    });
                }
                return fail(500, {
                    message: "Server error. Try again later.",
                    form: form,
                });
            }

            console.log("Sign in with email & password successful: ", data)
            redirect(307, '/home');

        } else if (selectedLoginMethod === 'emailCode') {

            const form = await superValidate(formData, joi(passwordlessLoginSchema))

            if (!form.valid) {
                console.error("Validaton Error:", form);
                return fail(400, { form });
            }
            console.log("formdata:", form);
            const { data, error } = await locals.supabase.auth.signInWithOtp({
                email: form.data.email,
                options: {
                    shouldCreateUser: false,
                }
            });

            if (error) {
                if (error instanceof AuthApiError && error.status === 400) {
                    return fail(400, {
                        error: "Invalid credentials",
                        email: form.data.email,
                        invalid: true,
                        message: err.message,
                        form: form,
                        apiAuthError: true
                    });
                }
                return fail(500, {
                    message: "Server error. Try again later.",
                    form: form,
                });
            }

            console.log(`One-Time-Password was sent to ${form.data.email}: `, data);

            redirect(307, `/verify-otp?email=${email}`);
        } else if (selectedLoginMethod === 'google') {
            // console.log("SESSION (Before):", request.locals.session);    
            
            const { data, error } = await locals.supabase.auth.signInWithOAuth({
                provider: "google",
                options: {
                    redirectTo: 'http://localhost:5173/home'
                }
            });

            // console.log("SESSION (After Update):", request.locals.session);

            if (error) {
                console.error("Google Sign-in Response (Error):", error); 
                return fail(500, {
                    message: "Error authenticating with Google.",
                });
            }

            redirect(307, data.url)
            
        }
    },
};

I have tried to get the data to client-side, without success. When there are invalid credentials this is logged in the console:

Invalid credentials AuthApiError: Invalid login credentials
    at handleError (/Users/leonardmarshall-afzal/Projects/platform--supabase/frontend/node_modules/@supabase/gotrue-js/dist/main/lib/fetch.js:43:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async _handleRequest (/Users/leonardmarshall-afzal/Projects/platform--supabase/frontend/node_modules/@supabase/gotrue-js/dist/main/lib/fetch.js:81:9)
    at async _request (/Users/leonardmarshall-afzal/Projects/platform--supabase/frontend/node_modules/@supabase/gotrue-js/dist/main/lib/fetch.js:65:18)
    at async SupabaseAuthClient.signInWithPassword (/Users/leonardmarshall-afzal/Projects/platform--supabase/frontend/node_modules/@supabase/gotrue-js/dist/main/GoTrueClient.js:300:23)
    at async default (/Users/leonardmarshall-afzal/Projects/platform--supabase/frontend/src/routes/login/+page.server.js:63:37)
    at async Module.handle_action_json_request (/Users/leonardmarshall-afzal/Projects/platform--supabase/frontend/node_modules/@sveltejs/kit/src/runtime/server/page/actions.js:48:18)
    at async resolve (/Users/leonardmarshall-afzal/Projects/platform--supabase/frontend/node_modules/@sveltejs/kit/src/runtime/server/respond.js:457:18)
    at async Module.respond (/Users/leonardmarshall-afzal/Projects/platform--supabase/frontend/node_modules/@sveltejs/kit/src/runtime/server/respond.js:330:20)
    at async file:///Users/leonardmarshall-afzal/Projects/platform--supabase/frontend/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:524:22 {
  __isAuthError: true,
  status: 400
}

And this is the response in the network tab of the browser developer tools:

{
    "type": "failure",
    "status": 400,
    "data": "[{\"error\":1,\"email\":2,\"invalid\":3,\"message\":4,\"form\":5,\"authApiError\":3},\"Invalid credentials\",\"[email protected]\",true,\"Invalid login credentials\",{\"id\":6,\"valid\":3,\"posted\":3,\"errors\":7,\"data\":8},\"dz8vqv\",{},{\"email\":2,\"password\":9,\"loginMethod\":10},\"Password123!\",\"email\"]"
}

How do I access the this information in +page.svelte to so that an error message can be displayed informing the user of the problem.

1 Answer 1

1

I've just been struggling with the same problem and found your post here.

I've used message helper from SuperForms (here are the docs: https://superforms.rocks/concepts/messages#status-messages)

In the +page.server.ts, where you're looking for errors, I have replaced the return fail with return message:

//+page.server.ts

...

if (error) {
  if (error instanceof AuthApiError && error.status === 400) {
    return message(form, 'Invalid email or password');
    }
}


...

Also in +page.svelte add message to superForm and call it in {#if} statement:

//+page.svelte

<script>
...

  const { form, errors, enhance, message } = superForm(data.form);

...
</script>

<form>
...

{#if $message}
  <small>{$message}</small>
{/if}

</form>

It's my first time developing an authentication website, so if there's anyone with more experience, please correct me, if there's a better way.

I hope this helps. Take care!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.