Saturday, July 2, 2022
HomeWeb DevelopmentDealing with person authentication with Remix

Dealing with person authentication with Remix


Remix is a fullstack React framework with APIs that assist server rendering, knowledge loading, and routing. It makes use of the Internet Fetch API to allow quick web page hundreds and seamless transitions between a number of websites, and it may possibly run wherever.

As we speak, we’ll learn to handle person authentication in Remix on this tutorial. We’ll create a quote wall utility the place authenticated customers can view and publish quotes, whereas unauthenticated customers can simply view the posts and haven’t any capacity to put up.

Desk of Contents

Stipulations

To observe together with this text, you have to have the next:

  • Node.js 14 or better
  • npm 7 or better
  • Prior information of HTML, CSS, JavaScript, React
  • Fundamental information of Tailwind CSS

Organising a quote wall app with Remix

Creating a brand new Remix app

To get began, it’s essential to decide on Simply the fundamentals, Remix App Server, after which TypeScript when prompted.

Let’s scaffold a fundamental Remix utility with the next command:

npx [email protected] remix-quote-wall
cd remix-quote-wall
npm run dev

Organising Tailwind

So as to add Tailwind to our Remix app, let’s do the next:

  1. Set up Tailwind

Set up tailwindcss, its peer dependencies, and concurrently by way of npm, after which run the init command to generate each tailwind.config.js and postcss.config.js:

npm set up -D tailwindcss postcss autoprefixer concurrently
npx tailwindcss init -p
  1. Configure your template paths

Add the paths to your whole template information in your tailwind.config.js file:

//tailwind.config.js

module.exports = {
  content material: [
    "./app/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    prolong: {},
  },
  plugins: [],
}
  1. Replace your bundle.json scripts

Replace the scripts in your bundle.json file to construct each your improvement and manufacturing CSS:

...
{
  "scripts": {
    "construct": "npm run construct:css && remix construct",
    "construct:css": "tailwindcss -m -i ./types/app.css -o app/types/app.css",
    "dev": "concurrently "npm run dev:css" "remix dev"",
    "dev:css": "tailwindcss -w -o ./app/types/app.css",
  }
}
...
  1. Add the Tailwind directives to your CSS

Create a ./types/app.css file and add the @tailwind directives for every of Tailwind’s layers:

/*app.css*/

@tailwind base;
@tailwind parts;
@tailwind utilities;
  1. Import CSS file

Import the compiled ./app/types/app.css file in your ./app/root.jsx file by including the next:

...
import types from "~/types/app.css"

export perform hyperlinks() {
  return [{ rel: "stylesheet", href: styles }]
        }
...

Now, let’s run our improvement server with the next command:

npm run dev

Organising root.jsx

Let’s add some Tailwind courses to the app part by changing the contents of app/root.jsx with this:

// app/root.jsx

import {
    Hyperlinks,
    LiveReload,
    Meta,
    Outlet
} from "@remix-run/react";
import types from "./types/app.css"

export perform hyperlinks() {
    return [{ rel: "stylesheet", href: styles }]
}

export const meta = () => ({
    charset: "utf-8",
    title: "Quote Wall App",
    viewport: "width=device-width,initial-scale=1",
});

export default perform App() {
    return (
        <html lang="en">
            <head>
                <Meta />
                <Hyperlinks />
            </head>
            <physique className="bg-purple-100 relative px-5">
                <div className="mt-20 w-full max-w-screen-lg mx-auto">
                    <Outlet />
                </div>
                <LiveReload />
            </physique>
        </html>
    );
}

Routes

After that, let’s arrange our route construction. We’re going to have a couple of routes:

/
/login
/new-quote

Let’s begin with the index route (/). To do this, create a file app/routes/index.tsx and add the next to it:

export default perform Index() {

  return (
    <div>
      <div className="grid lg:grid-flow-row grid-cols-1 lg:grid-cols-3">
        Whats up
      </div>
    </div>
  )
}

Then, let’s replace our app/root.tsx file with the next:

import {
  Hyperlink,
  Hyperlinks,
  LiveReload,
  Meta,
  Outlet
} from "@remix-run/react";
import types from "./types/app.css"

export perform hyperlinks() {
  return [{ rel: "stylesheet", href: styles }]
}

export const meta = () => ({
  charset: "utf-8",
  title: "Quote Wall App",
  viewport: "width=device-width,initial-scale=1",
});

export default perform App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Hyperlinks />
      </head>
      <physique className="bg-purple-100 relative px-5">
        <div className="mt-20 w-full max-w-screen-lg mx-auto">
          <Outlet />
        </div>
        <LiveReload />
      </physique>
    </html>
  );
}

Our app is getting in form! We are going to create the lacking routes as we progress.

The navigation part

Let’s replace our app/routes/index.jsx file with the navigation phase as follows:

import { Hyperlink } from "@remix-run/react";

export default perform Index() {

    return (
      <div>
        <nav className="bg-gradient-to-br from-purple-400 via-purple-500 to-purple-500 w-full mounted top-0 left-0 px-5">
          <div
            className="w-full max-w-screen-lg mx-auto flex justify-between content-center py-3 ">
            <Hyperlink className="text-white text-3xl font-bold" to={"https://weblog.logrocket.com/"}>Quote Wall</Hyperlink>
            <div className="flex flex-col md:flex-row items-center justify-between gap-x-4 text-blue-50">

              <Hyperlink to={"login"}>Login</Hyperlink>
              <Hyperlink to={"login"}>Register</Hyperlink>

              <Hyperlink to={"new-quote"}>Add A Quote</Hyperlink>
              <Hyperlink to={"logout"}>Logout</Hyperlink>
            </div>
          </div>

        </nav>
      </div>
    )
}

A quote phase

Since we received’t be utilizing our quote part in multiple file, we’ll add it within the app/routes/index.jsx file.

Let’s replace our app/routes/index.jsx file with the quote phase as follows:

...

export default perform Index() {

    return (
        <div>
          <nav>...</nav>
            <div className="grid lg:grid-flow-row grid-cols-1 lg:grid-cols-3">
                <determine className="m-4 py-10 px-4 shadow-md shadow-sky-100">
                    <blockquote cite="https://wisdomman.com" className="py-3">
                        <p className="text-gray-800  text-xl">
                            A sew in Time saves 9.
                        </p>
                    </blockquote>
                    <figcaption>
                        <cite className="text-gray-600 text-md mb-4 text-right">
                            - Unknown
                        </cite>
                    </figcaption>
                </determine>
            </div>
        </div>
    )
}

Let’s add some further quotes to our program utilizing dummy knowledge.

We’ll want to write down a loader perform to help with knowledge loading and provisioning. Add the next to our app/routes/index.jsx file:

// app/routes/index.jsx
import { Hyperlink, useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/node";

export const loader = async () => {

    return json({
        quotes: [
            {
                quote: 'Light at the end of the tunnel, dey don cut am.',
                by: 'Brain Jotter'
            },
            {
                quote: 'Promised to stand by you, we don sit down.',
                by: 'Brain Jotter'
            },
            {
                quote: 'Polythecnic wey dey in Italy, Napoli.',
                by: 'Comrade with wisdom and Understanding'
            }
        ]
    })
};
...

Right here, we import the useLoaderData from '@remix-run/react Hook so we are able to entry the offered knowledge from the loader perform. Additionally, we import { json } from ‘@remix-run/node to be able to return knowledge in JSON format.

Now, let’s populate the web page with the quotes. With the information offered, let’s populate it on the web page with the map perform:

// app/routes/index.jsx
...
export default perform Index() {
  const { quotes } = useLoaderData();

  return (
    <div>
      <nav>...</nav>
      <div className="grid lg:grid-flow-row grid-cols-1 lg:grid-cols-3">
        {
          quotes.map((q, i) => {
            const { quote, by } = q;
            return (
              <determine key={i} className="m-4 py-10 px-4 shadow-md shadow-sky-100">
                <blockquote cite="https://wisdomman.com" className="py-3">
                  <p className="text-gray-800  text-xl">
                    {quote}
                  </p>
                </blockquote>
                <figcaption>
                  <cite className="text-gray-600 text-md mb-4 text-right">
                    - {by}
                  </cite>
                </figcaption>
              </determine>
            )
          })
        }
      </div>
    </div>
  )
}

Organising the database

Information persistence is required in virtually all real-world purposes. We’d like to avoid wasting our quotes to a database in order that others can learn them and probably submit their very own.

Organising Prisma

We’ll make the most of our personal SQLite database on this article. It’s mainly a database that sits in a file in your pc, is remarkably succesful, and better of all is supported by Prisma. In case you’re undecided which database to make the most of, this is a superb place to begin.

To get began, we’ll have to put in the next packages:

  • prisma for interacting with our database and schema throughout improvement
  • @prisma/shopper for making queries to our database throughout runtime

Set up the Prisma packages:

npm set up --save-dev prisma
npm set up @prisma/shopper

And we are able to now initialize Prisma with SQLite:

npx prisma init --datasource-provider sqlite

The next is what you’re going to get:

✔ Your Prisma schema was created at prisma/schema.prisma
  Now you can open it in your favourite editor.

warn You have already got a .gitignore. Do not forget to exclude .env to not commit any secret.

Subsequent steps:
1. Set the DATABASE_URL within the .env file to level to your present database. In case your database has no tables but, learn https://pris.ly/d/getting-started
2. Run prisma db pull to show your database schema right into a Prisma schema.
3. Run prisma generate to generate the Prisma Shopper. You possibly can then begin querying your database.

Extra data in our documentation:

Getting began

Getting began

We should always discover new information and folders like prisma/schema.prisma after operating the command above.

Our prisma/schema.prisma ought to appear to be this:

// prisma/schema.prisma
// That is your Prisma schema file,
// be taught extra about it within the docs: https://pris.ly/d/prisma-schema

generator shopper {
  supplier = "prisma-client-js"
}

datasource db {
  supplier = "sqlite"
  url      = env("DATABASE_URL")
}

Now that Prisma is put in and arrange, let’s start modeling our app.

Replace prisma/schema.prisma with the next:

// prisma/schema.prisma
// That is your Prisma schema file,
// be taught extra about it within the docs: https://pris.ly/d/prisma-schema

generator shopper {
  supplier = "prisma-client-js"
}

datasource db {
  supplier = "sqlite"
  url      = env("DATABASE_URL")
}

mannequin Quote {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  by        String
  quote     String
}

With this, let’s run the next command:

npx prisma db push

The command’s output will likely be as follows:

Setting variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"

SQLite database dev.db created at file:./dev.db

🚀  Your database is now in sync together with your schema. Achieved in 158ms

✔ Generated Prisma Shopper (3.14.0 | library) to ./node_modules/@prisma/shopper in
 1.44s

What occurred was that our database prisma/dev.db was created first, after which all the important modifications had been made to the database to mirror the schema that we specified within the (prisma/schema.prisma) file. Lastly, it constructed Prisma’s TypeScript sorts in order that once we use its API to attach with the database, we’ll get improbable autocomplete and sort checking.

Subsequent, let’s add that prisma/dev.db and .env to our .gitignore so we don’t commit them to our GitHub repository:

node_modules
.output
.cache

/.cache
/construct/
/public/construct
/api/index.js

`

In case your database will get tousled, you possibly can delete the prisma/dev.db file and run the npx prisma db push command once more.

Connecting to the database

That is how we join our app with the database. We’ll add the next on the high of our prisma/seed.ts file, which we’ll create later as we progress:

import { PrismaClient } from "@prisma/shopper";
const db = new PrismaClient();

Whereas this works completely, we don’t wish to should shut down and restart our server each time we make a server-side modification throughout improvement. Consequently, @remix-run/serve rebuilds the code from the bottom up.

The issue with this technique is that each time we alter the code, we’ll create a brand new database connection, and we’ll quickly run out of connections. With database-accessing apps, it is a prevalent drawback. Consequently, Prisma points a warning:

Warning: 10 Prisma Shoppers are already operating

To keep away from this improvement time drawback, we’ve bought just a little bit of additional work to do.

Create a brand new file app/utils/db.server.ts and paste the next code into it:

import { PrismaClient } from "@prisma/shopper";

let db: PrismaClient;

declare world  undefined;


// that is wanted as a result of in improvement we do not wish to restart
// the server with each change, however we wish to make certain we do not
// create a brand new connection to the DB with each change both.
if (course of.env.NODE_ENV === "manufacturing") {
  db = new PrismaClient();
} else {
  if (!world.__db) {
    world.__db = new PrismaClient();
  }
  db = world.__db;
}

export { db };

The file naming conference is one factor I’d wish to level out. The .server piece of the filename tells Remix that this code ought to by no means be displayed in a browser. This isn’t required as a result of Remix does a improbable job at preserving server code out of the shopper.

Nevertheless, as a result of some server-only dependencies will be troublesome to tree shake, appending .server to the filename tells the compiler to disregard this module and its imports when bundling for the browser. For the compiler, the .server works as a kind of barrier.

Let’s create a brand new file referred to as prisma/seed.ts and paste the next code snippet:

import { PrismaClient } from "@prisma/shopper";

const db = new PrismaClient();

async perform seed() {
    await Promise.all(
        getQuotes().map((quote) => {
            return db.quote.create({ knowledge: quote })
        })
    )
}
seed();

perform getQuotes() {
    return [
        {
            quote: 'The greatest glory in living lies not in never falling, but in rising every time we fall.',
            by: 'Nelson Mandela'
        },
        {
            quote: 'The way to get started is to quit talking and begin doing.',
            by: 'Walt Disney'
        },
        {
            quote: "Your time is limited, so don't waste it living someone else's life. Don't be trapped by dogma – which is living with the results of other people's thinking.",
            by: 'Steve Jobs'
        },
        {
            quote: "If life were predictable it would cease to be life, and be without flavor.",
            by: 'Eleanor Roosevelt'
        },
        {
            quote: "If you look at what you have in life, you'll always have more. If you look at what you don't have in life, you'll never have enough.",
            by: 'Oprah Winfrey'
        },
        {
            quote: "If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success.",
            by: 'James Cameron'
        },
        {
            quote: "Life is what happens when you're busy making other plans.",
            by: 'John Lennon'
        }
    ]
}

You might be welcome to contribute new quotes to this record.

We should now run this program to be able to seed our database with dummy quotes.

Set up esbuild-register as a dev dependency to be able to run the seed file:

npm set up --save-dev esbuild-register

And now we are able to run our seed.ts file with the next command:

node --require esbuild-register prisma/seed.ts

The dummy quotes have now been seeded into our database.
We don’t have to run the above command every time we reset the database, so we’ll put it to our bundle.json file:

// ...
  "prisma": {
    "seed": "node --require esbuild-register prisma/seed.ts"
  },
  "scripts": {
// ...

Now, any time we reset the database, Prisma will name our seed file as effectively.

Fetching quotes from the database utilizing the Remix loader

Our intention is to place a listing of the quotes on the / route

A loader is used to load knowledge right into a Remix route module. That is primarily an async perform you export that returns a response and is accessed by way of the useLoaderData hook on the part.

Let’s make some modifications to app/route/index.tsx:

...
import { db } from "~/utils/db.server";


export const loader = async () => {

  return json({
    quotes: await db.quote.findMany()
  })
};

export default perform Index() {
  const { quotes } = useLoaderData();

  return (
    <div>
      <nav></nav>
      <div className="grid lg:grid-flow-row grid-cols-1 lg:grid-cols-3">
        {
          quotes.map((q, i) => {
            const { id, quote, by } = q;
            return (
              <determine key={id} className="m-4 py-10 px-4 shadow-md shadow-sky-100">
                <blockquote  className="py-3">
                  <p className="text-gray-800  text-xl">
                    {quote}
                  </p>
                </blockquote>
                <figcaption>
                  <cite className="text-gray-600 text-md mb-4 text-right">
                    - {by}
                  </cite>
                </figcaption>
              </determine>
            )
          })
        }
      </div>
    </div>
  )
}

Run npm run dev and here’s what you’re going to get:

Quote Wall

Creating new quotes

Let’s construct a method so as to add new quotes to the database now that we’ve been in a position to show them from the database storage, we could?

Create app/routes/new-quote.tsx file and add the next to the file:

const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg text-purple-900 outline-purple-300 `;
export default perform NewQuoteRoute() {
    return (
      <div className="flex justify-center items-center content-center">
            <div className="lg:m-10 my-10 md:w-2/3 lg:w-1/2 bg-gradient-to-br from-purple-500 via-purple-400 to-purple-300  font-bold px-5 py-6 rounded-md">
                <kind methodology="put up">
                    <label className="text-lg leading-7 text-white">
                        Quote Grasp (Quote By):
                        <enter
                            sort="textual content"
                            className={inputClassName}
                            title="by"
                            required
                        />
                    </label>
                    <label className="text-lg leading-7 text-white">
                        Quote Content material:
                        <textarea required className={`${inputClassName} resize-none `} id="" cols={30} rows={10} title="quote"></textarea>
                    </label>
                    <button className="my-4 py-3 px-10 text-purple-500 font-bold border-4 hover:scale-105 border-purple-500 rounded-lg bg-white" sort="submit">Add</button>
                </kind>
            </div>
        </div>
    )
}

Right here is the what the shape web page appears to be like like:

Quote Wall Fields

Let’s replace the app/routes/new-quote.tsx file with the next to be able to submit knowledge.

import { redirect } from "@remix-run/node";
import { db } from "~/utils/db.server";

export const motion = async ({ request }) => {

    const kind = await request.formData();
    const by = kind.get('by');
    const quote = kind.get('quote');

    if (
        typeof by !== "string" || by === "" ||
        typeof quote !== "string" || quote === ""
    ) {
        redirect('/new-quote')
        throw new Error(`Type not submitted accurately.`);
    }
    const fields = { by, quote };

    await db.quote.create({ knowledge: fields });
    return redirect("https://weblog.logrocket.com/");
}
...

The motion methodology known as for POST, PATCH, PUT, and DELETE HTTP strategies, and it’s used to edit or mutate knowledge. The request attribute offers us entry to the shape knowledge so we are able to validate it and submit the request to the server.

We will now add quotes, which can take us to the principle web page, which can show the brand new quotes we’ve added.

Discover how we processed the shape submission while not having to make use of any React Hooks.

Username/password authentication

Let’s have a look at how we are able to deal with authentication to limit unregistered customers from posting.

On this article, we’ll implement the standard username/password authentication technique.

We’ll be utilizing bcryptjs to hash our passwords so no one will be capable of fairly brute-force their method into an account.

We’ll set up the bcrypt library and its sort definition as follows:

npm set up bcryptjs
npm set up --save-dev @sorts/bcryptjs

We now have to replace the prisma/schema.prisma file with the person mannequin and it ought to appear to be this:

generator shopper {
  supplier = "prisma-client-js"
}
datasource db {
  supplier = "sqlite"
  url      = env("DATABASE_URL")
}
mannequin Consumer {
  id           String   @id @default(uuid())
  createAt     DateTime @default(now())
  updatedAt    DateTime @updatedAt
  username     String   @distinctive
  passwordHash String
  quotes       Quote[]
}
mannequin Quote {
  id        String   @id @default(uuid())
  userId    String
  addedBy   Consumer     @relation(fields: [userId], references: [id])
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  by        String
  quote     String
}

Subsequent, we’ll reset our database with this schema by operating the next command:

npx prisma db push

Operating this command will immediate you to reset the database, hit Y to verify.

Subsequent, we’ll replace the seed perform in our prisma/seed.ts file as follows:

...
async perform seed() {
    // WisdomMan is a default person with the password   'twixrox'
    const wisdomMan = await db.person.create({
        knowledge: {
          username: "WisdomMan",
          // it is a hashed model of "twixrox"
          passwordHash:
            "$2b$10$K7L1OJ45/4Y2nIvhRVpCe.FSmhDdWoXehVzJptJ/op0lSsvqNu/1u",
        },
      });
    await Promise.all(
        getQuotes().map((quote) => {
            const knowledge = {userId:wisdomMan.id, ...quote}
            return db.quote.create({ knowledge })
        })
    )
}
seed();
...

Right here, we seed in a person with the username “WisdomMan” and the password hash of “twixrox”.” Additionally, we seed the database with all our dummy quotes.

We now have to run the seed once more with the next:

npx prisma db seed

Type validation with motion

Let’s create a /login route by including a app/routes/login.tsx file with the next validation logics for our login and registration kinds:

import { json } from "@remix-run/node";

perform validateUsername(username: unknown) {
    if (typeof username !== "string" || username.size < 3) {
        return `Usernames have to be a minimum of 3 characters lengthy`;
    }
}
perform validatePassword(password: unknown) {
    if (typeof password !== "string" || password.size < 6) {
        return `Passwords have to be a minimum of 6 characters lengthy`;
    }
}
perform validateUrl(url: any) {
    console.log(url);
    let urls = ["https://blog.logrocket.com/"];
    if (urls.contains(url)) {
        return url;
    }
    return "https://weblog.logrocket.com/";
}
const badRequest = (knowledge: any) =>
    json(knowledge, { standing: 400 }
);

Right here, we wrote some customized validation logic for username, password and the url.

Subsequent, we’ll replace the app/routes/login.tsx file with the next JSX template:

import sort {
  ActionFunction,
  LinksFunction,
} from "@remix-run/node";
import {
  useActionData,
  Hyperlink,
  useSearchParams,
} from "@remix-run/react";
import { db } from "~/utils/db.server";
...

const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg text-purple-900 outline-purple-300 `;
export default perform LoginRoute() {
    const actionData = useActionData();
    const [searchParams] = useSearchParams();
    return (
        <div className="flex justify-center items-center content-center text-white">
            <div className="lg:m-10 my-10 md:w-2/3 lg:w-1/2 bg-gradient-to-br from-purple-500 via-purple-400 to-purple-300  font-bold px-5 py-6 rounded-md">
                <kind methodology="put up">
                    <h1 className="text-center text-2xl text-white">Login</h1>
                    <enter
                        sort="hidden"
                        title="redirectTo"
                        worth={
                            searchParams.get("redirectTo") ?? undefined
                        }
                    />
                    <fieldset className="text-center ">
                        <legend className="sr-only">
                            Login or Register?
                        </legend>
                        <label>
                            <enter
                                sort="radio"
                                title="loginType"
                                worth="login"
                                defaultChecked=
                            />{" "}
                            Login
                        </label>
                        <label>
                            <enter
                                sort="radio"
                                title="loginType"
                                worth="register"
                                defaultChecked={
                                    actionData?.fields?.loginType ===
                                    "register"
                                }
                            />{" "}
                            Register
                        </label>
                    </fieldset>
                    <label className="text-lg leading-7 text-white">
                        Username:
                        <enter
                            sort="textual content"
                            className={inputClassName}
                            title="username"
                            required
                            minLength={3}
                            defaultValue={actionData?.fields?.username}
                            aria-invalid={Boolean(
                                actionData?.fieldErrors?.username
                            )}
                            aria-errormessage={
                                actionData?.fieldErrors?.username
                                    ? "username-error"
                                    : undefined
                            }
                        />
                        {actionData?.fieldErrors?.username ? (
                            <p
                                className="text-red-500"
                                position="alert"
                                id="username-error"
                            >
                                {actionData.fieldErrors.username}
                            </p>
                        ) : null}
                    </label>
                    <label className="text-lg leading-7 text-white">
                        Password
                        <enter
                            title="password"
                            className={inputClassName}
                            required
                            defaultValue={actionData?.fields?.password}
                            sort="password"
                            aria-invalid= undefined
                            
                            aria-errormessage={
                                actionData?.fieldErrors?.password
                                    ? "password-error"
                                    : undefined
                            }
                        />
                        {actionData?.fieldErrors?.password ? (
                            <p
                                className="text-red-500"
                                position="alert"
                                id="password-error"
                            >
                                {actionData.fieldErrors.password}
                            </p>
                        ) : null}
                    </label>
                    <div id="form-error-message">
                        {actionData?.formError ? (
                            <p
                                className="text-red-500"
                                position="alert"
                            >
                                {actionData.formError}
                            </p>
                        ) : null}
                    </div>
                    <button className="my-4 py-2 px-7 text-purple-500 font-bold border-2 hover:scale-105 border-purple-500 rounded-lg bg-white" sort="submit">Login</button>
                </kind>
            </div>
        </div>
    )
}

Right here, we use useSearchParams to get the redirectTo question parameter and placing that in a hidden enter. This fashion, our motion can know the place to redirect the person. We’ll use this to redirect a person to the login and registration web page. We added some situations to our JSX to show error messages within the kind if any happens.

Creating session helpers

Earlier than creating our session helpers, let’s add session secret to our .env file as follows:

SESSION_SECRET=secret

Let’s create a file referred to as app/utils/session.server.ts and add the next session helper capabilities:

import bcrypt from "bcryptjs";
import {
    createCookieSessionStorage,
    redirect,
} from "@remix-run/node";
import { db } from "./db.server";
const sessionSecret = course of.env.SESSION_SECRET;
if (!sessionSecret) {
    throw new Error("SESSION_SECRET have to be set");
}
const storage = createCookieSessionStorage({
    cookie: {
        title: "RJ_session",
        // usually you need this to be `safe: true`
        // however that does not work on localhost for Safari
        // https://net.dev/when-to-use-local-https/
        safe: course of.env.NODE_ENV === "manufacturing",
        secrets and techniques: [sessionSecret],
        sameSite: "lax",
        path: "https://weblog.logrocket.com/",
        maxAge: 60 * 60 * 24 * 30,
        httpOnly: true,
    },
});
export async perform createUserSession(
    userId: string,
    redirectTo: string
) {
    const session = await storage.getSession();
    session.set("userId", userId);
    return redirect(redirectTo, {
        headers: {
            "Set-Cookie": await storage.commitSession(session),
        },
    });
}
perform getUserSession(request: Request) {
    return storage.getSession(request.headers.get("Cookie"));
}

Right here, we create our session storage utilizing the createCookieSessionStorage methodology. The createUserSession perform will get the saved session and units it to our distinctive person ID and units the cookie to the request header. The getUser perform retrieves the person cookie from the request headers.

Subsequent, we’ll add helper capabilities to retrieve customers by their distinctive ID.

Add the next to the app/utils/session.server.ts file:

...

export async perform getUserId(request: Request) 

export async perform getUser(request: Request) {
  const userId = await getUserId(request);
  if (typeof userId !== "string") {
    return null;
  }
  attempt {
    const person = await db.person.findUnique({
      the place: { id: userId },
      choose: { id: true, username: true },
    });
    return person;
  } catch {
    throw logout(request);
  }
}

Right here, the getUserId perform retrieves the person id from the prevailing session whereas the getUser perform makes use of the retrieved person ID to question the database for a person with an identical ID. We’ll implement the logout session helper as we proceed.

Subsequent, we’ll create a helper perform to stop unauthenticated customers from creating quotes.

Add the next to the app/utils/session.server.ts file:

export async perform requireUserId(
  request: Request,
  redirectTo: string = new URL(request.url).pathname
) {
  const session = await getUserSession(request);
  const userId = session.get("userId");
  if (!userId || typeof userId !== "string") {
    const searchParams = new URLSearchParams([
      ["redirectTo", redirectTo],
    ]);
    throw redirect(`/login?${searchParams}`);
  }
  return userId;
}

With the next implementation, customers who should not signed in will likely be redirected to the login route each time they attempt to create a quote.

Subsequent, the login, register, and logout helper capabilities.

Add the next to the app/utils/session.server.ts file:

...
sort LoginForm = {
  username: string;
  password: string;
};

export async perform register({
  username,
  password,
}: LoginForm) {
  const passwordHash = await bcrypt.hash(password, 10);
  const person = await db.person.create({
    knowledge: { username, passwordHash },
  });
  return { id: person.id, username };
}

export async perform login({
  username,
  password,
}: LoginForm) {
  const person = await db.person.findUnique({
    the place: { username },
  });
  if (!person) return null;
  const isCorrectPassword = await bcrypt.examine(
    password,
    person.passwordHash
  );
  if (!isCorrectPassword) return null;
  return { id: person.id, username };
}

export async perform logout(request: Request) {
  const session = await getUserSession(request);
  return redirect("/login", {
    headers: {
      "Set-Cookie": await storage.destroySession(session),
    },
  });
}

The register perform makes use of bcrypt.hash to hash the password earlier than we retailer it within the database after which return the person ID and username. The login perform question the database by username. If discovered, the bcrypt.examine methodology is used to check the password with the passwordhash then return the person id and username. The logout perform destroys the prevailing person session and redirects to the login route.

Processing login and register kind submissions

It’s best to have a good information on the right way to deal with kind submission since we’ve achieved the identical within the create new quote part.

Equally, we’ll create an motion methodology that may settle for the request object, which is used to switch or mutate knowledge on the server.

Now, let’s replace the app/routes/login.tsx file with the next:

import { createUserSession, login, register } from "~/utils/session.server";
...
export const motion: ActionFunction = async ({ request }) => {
    const kind = await request.formData();
    const loginType = kind.get("loginType");
    const username = kind.get("username");
    const password = kind.get("password");
    const redirectTo = validateUrl(
        kind.get("redirectTo") || "https://weblog.logrocket.com/"
    );
    if (
        typeof loginType !== "string" ||
        typeof username !== "string" ||
        typeof password !== "string" ||
        typeof redirectTo !== "string"
    ) {
        return badRequest({
            formError: `Type not submitted accurately.`,
        });
    }
    const fields = { loginType, username, password };
    const fieldErrors = {
        username: validateUsername(username),
        password: validatePassword(password),
    };
    if (Object.values(fieldErrors).some(Boolean))
        return badRequest({ fieldErrors, fields });
    change (loginType) {
        case "login": {
            const person = await login({ username, password });
            console.log({ person });
            if (!person) {
                return badRequest({
                    fields,
                    formError: `Username/Password mixture is inaccurate`,
                });
            }
            return createUserSession(person.id, redirectTo);
        }
        case "register": {
            const userExists = await db.person.findFirst({
                the place: { username },
            });
            if (userExists) {
                return badRequest({
                    fields,
                    formError: `Consumer with username ${username} already exists`,
                });
            }
            const person = await register({ username, password });
            if (!person) {
                return badRequest({
                    fields,
                    formError: `One thing went flawed attempting to create a brand new person.`,
                });
            }
            return createUserSession(person.id, redirectTo);
        }
        default: {
            return badRequest({
                fields,
                formError: `Login sort invalid`,
            });
        }
    }
};

Right here, we wrote a management circulate utilizing the change assertion for each login and register instances. For the login circulate, if there’s no person, the fields and a formError will likely be returned. If there’s a person, we’ll create their session and redirect to /quotes. For the register circulate, we verify if the person exists. If there’s no person, we’ll create one alongside a session and redirect to /.

Making a logout function

Let’s create a file referred to as app/routes/logout.tsx and add the next:

import sort { ActionFunction, LoaderFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { logout } from "~/utils/session.server";
export const motion: ActionFunction = async ({ request }) => {
  return logout(request);
};
export const loader: LoaderFunction = async () => {
  return redirect("https://weblog.logrocket.com/");
};

Replace the app/routes/index.tsx file with the next:

...
import { getUser } from "~/utils/session.server";

export const loader = async ({ request }) => {
  const person = await getUser(request);
  return json({
    quotes: await db.quote.findMany(),
    person
  })
};
export default perform Index() {
  const { quotes, person } = useLoaderData();
  return (
    <div>
      <nav className="bg-gradient-to-br from-purple-400 via-purple-500 to-purple-500 w-full mounted top-0 left-0 px-5">
        <div
          className="w-full max-w-screen-lg mx-auto flex justify-between content-center py-3 ">
          <Hyperlink className="text-white text-3xl font-bold" to={"https://weblog.logrocket.com/"}>Quote Wall</Hyperlink>
          <div className="flex flex-row items-center justify-between gap-x-4 text-blue-50">
            {
              person ? (
                <>
                  <Hyperlink to={"new-quote"}>Add A Quote</Hyperlink>

                  <kind motion="/logout" methodology="put up">
                    <button sort="submit" className="button">
                      Logout
                    </button>
                  </kind>
                </>) : (
                <>
                  <Hyperlink to={"login"}>Login</Hyperlink>
                  <Hyperlink to={"login"}>Register</Hyperlink>
                </>
              )
            }

          </div>
        </div >
      </nav >
      <div className="grid lg:grid-flow-row grid-cols-1 lg:grid-cols-3">
        ...
      </div>
    </div >
  )
}

Now that we’re achieved with all of the authentication logic, we’ll have to replace routes/new-quote in order that solely authenticated customers can create new quotes.

Replace the app/routes/new-quote.tsx file with the next:

import { redirect, json } from "@remix-run/node";
import { db } from "~/utils/db.server";
import { requireUserId, getUser } from "~/utils/session.server";
import { Hyperlink, useLoaderData } from "@remix-run/react";

export const motion = async ({ request }) => {
  const userId = await requireUserId(request);
  const kind = await request.formData();
  const by = kind.get("by");
  const quote = kind.get("quote");
  if (
    typeof by !== "string" ||
    by === "" ||
    typeof quote !== "string" ||
    quote === ""
  ) {
    redirect("/new-quote");
    throw new Error(`Type not submitted accurately.`);
  }
  const fields = { by, quote };
  await db.quote.create({
    knowledge: { ...fields, userId: userId },
  });
  return redirect("https://weblog.logrocket.com/");
};
export const loader = async ({ request }) => {
  const person = await getUser(request);
  return json({
    person,
  });
};

Subsequent, we’ll replace our TSX template as follows:

...

const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg text-purple-900 outline-purple-300 `;
export default perform NewQuoteRoute() {
    const { person } = useLoaderData();
  return (
    <>
      <nav className="bg-gradient-to-br from-purple-400 via-purple-500 to-purple-500 w-full mounted top-0 left-0 px-5">
        <div className="w-full max-w-screen-lg mx-auto flex justify-between content-center py-3 ">
          <Hyperlink className="text-white text-3xl font-bold" to={"https://weblog.logrocket.com/"}>
            Quote Wall
          </Hyperlink>
          <div className="flex flex-row items-center justify-between gap-x-4 text-blue-50">
            {person ? (
              <>
                <Hyperlink to={"new-quote"}>Add A Quote</Hyperlink>
                <kind motion="/logout" methodology="put up">
                  <button sort="submit" className="button">
                    Logout
                  </button>
                </kind>
              </>
            ) : (
              <>
                <Hyperlink to={"login"}>Login</Hyperlink>
                <Hyperlink to={"register"}>Register</Hyperlink>
              </>
            )}
          </div>
        </div>
      </nav>
      <div className="flex justify-center items-center content-center">
        <div className="lg:m-10 my-10 md:w-2/3 lg:w-1/2 bg-gradient-to-br from-purple-500 via-purple-400 to-purple-300  font-bold px-5 py-6 rounded-md">
          <kind methodology="put up">
            <label className="text-lg leading-7 text-white">
              Quote Grasp (Quote By):
              <enter
                sort="textual content"
                className={inputClassName}
                title="by"
                required
              />
            </label>
            <label className="text-lg leading-7 text-white">
              Quote Content material:
              <textarea
                required
                className={`${inputClassName} resize-none `}
                id=""
                cols={30}
                rows={10}
                title="quote"
              ></textarea>
            </label>
            <button
              className="my-4 py-3 px-10 text-purple-500 font-bold border-4 hover:scale-105 border-purple-500 rounded-lg bg-white"
              sort="submit"
            >
              Add
            </button>
          </kind>
        </div>
      </div>
    </>
  );
}

Now, solely the authenticated customers can create new quotes in our app whereas unauthenticated customers will likely be redirected to the /login route once they attempt to create a brand new quote. We will try the ultimate model of our venture by spinning up our improvement server with the next command:

npm run dev

Conclusion

We now have lastly come to the top of this tutorial. We now have taken a have a look at the right way to implement authentication on Remix apps, and we have now efficiently constructed a fullstack quote wall utility with assist for person authentication. There are such a lot of methods this may be improved, and I can’t wait to see what you construct subsequent with Remix. Thanks for studying.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments