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:
- 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
- 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: [], }
- 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", } } ...
- 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;
- 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:
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:
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.