Fullstack Authentication with Remix pomocí Prisma, MongoDB a Typescript

Remix je na první straně vykreslený JavaScript framework na straně serveru postavený na Reactu, který nám umožňuje vytvářet full-stack webové aplikace díky svým schopnostem frontendu a serveru. S mottem „Web Fundamentals, Modern UX“, protože jeho rozhraní API se v maximální možné míře řídí webovými standardy, jako jsou:odpovědi HTTP, odesílání formulářů, vestavěný zavaděč pro načítání dat a mnoho zajímavých funkcí.

V nedávném 2021 'Javascript Rising Stars' Remix byl zařazen mezi nejlepší full-stack framework, který si vývojáři vybrali. Remix získal hodně pozornosti (a 3 miliony dolarů počátečního financování, což také pomáhá!) a byl open source. Remix však není nový rámec, protože dříve byl k dispozici jako prémiový rámec založený na předplatném.

Co stavíme

Využijeme Remix spolu s MongoDB jako naši databázi s Prisma ORM pomocí Typescript a vytvoříme plně funkční autentizační aplikaci od začátku. K tomu využijeme funkci 'Vestavěná podpora pro soubory cookie' poskytovanou jako vestavěná funkce nazvaná createCookie pro práci se soubory cookie.

Předpoklady

  • Node.js 14+ (používá verzi 16.14.0)
  • npm 7+
  • Editor kódu

Vytvoření projektu

Příkazem nejprve inicializujeme nový projekt Remix

npx create-remix@latest

Náš projekt pojmenujeme a nazveme

remix-mongo-auth

Chceme také začít pouze se základní startovací šablonou a pokračovat ve zbytku instalačního procesu. Ke zpestření naší aplikace jsme také použili Tailwind. Startovací soubory naleznete v úložišti zde.

Připojování naší databáze

Pro naši databázi používáme MongoDB, což je nerelační databáze založená na dokumentech. Pro naši jednoduchost jej nakonfigurujeme pomocí Mongo Atlas a odtud vezmeme připojovací řetězec pro pozdější konfiguraci naší aplikace.

Upozorňujeme, že možná budete muset později aktivovat administrátorská práva vašeho uživatele, aby mohl provádět některé úlohy. To lze provést v nastavení přístupu k databázi.

Konfigurace PrismaORM

Začneme instalací závislosti Prisma dev, abychom mohli komunikovat s MongoDB a tlačit změny databáze.

npm i -D prisma

Tím se nám nainstaluje Prisma CLI. Potom chceme inicializovat prisma pomocí MongoDB (výchozí Postgres) pomocí příkazu

npx prisma init --datasource-provider mongodb

Nyní musíme vidět složku prisma vytvořenou v našem adresáři a uvnitř, která bude naše schema.prisma soubor vytvořený pro nás. Do souboru napíšeme jazyk prisma schema launguage, kde vytvoříme modely potřebné k implementaci autentizace.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(auto()) @map("_id") @db.ObjectId
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  email     String   @unique
  password  String
  profile   Profile
}

type Profile {
  fullName String
}

Zde jsme vytvořili model uživatele a model profilu. Uživatel bude mít svůj odkaz na dokument Profil.

Sloupec id je řetězec, který je automaticky generovanými hodnotami poskytnutými Mongo. @db.ObjectId je dát databázi jakékoli jedinečné ID. DateTime @default(now()) je aktuální časové razítko, které jsme poskytli CreateAt. Zbývající sloupce jsou pouze datovým typem, který poskytujeme datové struktuře.

Abychom viděli a odráželi změny v naší databázi, musíme přidat nový soubor, který bude zodpovědný za propojení naší databáze a aplikace Remix.

//utils/prisma.server.ts
import { PrismaClient } from "@prisma/client";
let prisma: PrismaClient;
declare global {
  var __db: PrismaClient | undefined;
}

if (process.env.NODE_ENV === "production") {
  prisma = new PrismaClient();
  prisma.$connect();
} else {
  if (!global.__db) {
    global.__db = new PrismaClient();
    global.__db.$connect(); 
  }
  prisma = global.__db;
}

export * from "@prisma/client";
export { prisma };

Výše uvedený úryvek je převzat z dokumentu Remix, kde vytvoří instanci nového klienta PrismaClient, pokud nebude nalezen žádný existující klient pro připojení k DB.

Nyní můžeme spustit příkaz pro použití změn schématu.

npx prisma db push   

Tím vytvoříte jakoukoli novou kolekci a indexy definované v našem schématu. Nyní můžeme zkontrolovat, zda všechny naše změny fungují. Můžeme spustit příkaz

npx prisma studio      

Tím se roztočí výchozí port, kde můžeme vidět odraz změn se sloupci, které se nám vytvoří. Což bude vypadat podobně jako níže

Přidání rozvržení

Chceme, aby naše aplikace měla standardní rozložení, do kterého můžeme zabalit veškerou aplikaci. To se hodí, pokud vytvoříme více rozvržení na více stránkách a předáme dětem rekvizitu.

export function Layout({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>;
}

Registrace uživatelů

Začněme přidávat registraci pro nové uživatele. Než začneme, budeme muset nainstalovat nějaké knihovny. Budeme potřebovat knihovnu, abychom ji mohli nainstalovat

npm i bcrypt

Tato knihovna nám pomůže zahašovat naše heslo před jeho uložením do naší databáze. Protože se opravdu nechceme chovat jako blázen a ukládat hesla ve formátu prostého textu v naší databázi. Chcete-li se dozvědět více o hašování pomocí bcrypt, přečtěte si tento článek zde.

Vytváření rozhraní typu

Protože používáme strojopis, začneme nejprve vytvořením typového rozhraní pro naše potřebné datové typy registrace. Níže je typ, který jsme vytvořili

//utils/types.server.ts
export type RegisterForm = {
  email: string;
  password: string;
  fullName?: string;
};

Nyní vytvoříme funkci, která převezme objekt uživatele, který obsahuje náš e-mail, heslo a celé jméno, a změní toto heslo na heslo hash, nakonec vytvoří nového uživatele v naší MongoDB.

//utils/user.server.ts
import bcrypt from "bcryptjs";
import type { RegisterForm } from "./types.server";
import { prisma } from "./prisma.server";

export const createUser = async (user: RegisterForm) => {
  const passwordHash = await bcrypt.hash(user.password, 10);
  const newUser = await prisma.user.create({
    data: {
      email: user.email,
      password: passwordHash,
      profile: {
        fullName: user.fullName,
      },
    },
  });
  return { id: newUser.id, email: user.email };
}; 

Nyní využijeme funkci cookie poskytovanou společností Remix. Což nám pomáhá generovat novou relaci cookie.

//utils/auth.server.ts
export async function createUserSession(userId: string, redirectTo: string) {
  const session = await storage.getSession();
  session.set("userId", userId);
  return redirect(redirectTo, {
    headers: {
      "Set-Cookie": await storage.commitSession(session),
    },
  });
}

Do tohoto okamžiku jsme vytvořili naši funkci createCookieSessionStorage, která vytvoří novou relaci cookie. Pojďme vytvořit tuto funkci

//utils/auth.server.ts

const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret) throw new Error("Secret not specified, it must be set");

const storage = createCookieSessionStorage({
  cookie: {
    name: "remix-mongo-auth",
    secure: process.env.NODE_ENV === "production",
    secrets: [sessionSecret],
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 60 * 24 * 30,
    httpOnly: true,
  },
});

Nyní máme vše potřebné k napsání naší funkce registerUser. Což zkontroluje existenci uživatele v databázi pomocí jedinečného e-mailu. Pokud existuje jedinečný e-mail, vytvoříme novou uživatelskou relaci, pokud ne, pošleme odpověď JSON s tím, že se něco pokazilo.

//utils/auth.server.ts
export async function registerUser(form: RegisterForm) {
  const userExists = await prisma.user.count({ where: { email: form.email } });
  if (userExists) {
    return json(
      { error: `User already exists with that email` },
      { status: 400 }
    );
  }

  const newUser = await createUser(form);
  if (!newUser) {
    return json(
      {
        error: `Something went wrong trying to create a new user.`,
        fields: { email: form.email, password: form.password, fullName: form.fullName },
      },
      { status: 400 }
    );
  }
  return createUserSession(newUser.id, "/");
}
//utils/auth.server.ts

export async function getUser(request: Request) {
  const userId = await getUserId(request);
  if (typeof userId !== "string") {
    return null;
  }

  try {
    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, email: true, profile: true },
    });
    return user;
  } catch {
    throw logout(request);
  }
}

function getUserSession(request: Request) {
  return storage.getSession(request.headers.get("Cookie"));
}

export async function 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(`/auth/login?${searchParams.toString()}`);
  }
  return userId;
}

Vytvoříme jednu další funkci, která nám vrátí uživatelské informace o uživateli, které nám byly vytvořeny.

//utils/user.server.ts
async function getUserId(request: Request) {
  const session = await getUserSession(request);
  const userId = session.get("userId");
  if (!userId || typeof userId !== "string") return null;
  return userId;
}

export async function getUser(request: Request) {
  const userId = await getUserId(request);
  if (typeof userId !== "string") {
    return null;
  }

  try {
    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, email: true, profile: true },
    });
    return user;
  } catch {
    throw logout(request);
  }
}

Poté, co je zapsáno vše potřebné k vytvoření nové uživatelské funkce. Vytvoříme několik nových souborů v naší složce tras.

//routes/index.ts
import { LoaderFunction, redirect } from '@remix-run/node';
import { requireUserId } from '~/utils/auth.server';

export const loader: LoaderFunction = async ({ request }) => {
  await requireUserId(request);
  return redirect('/home');
};

Uvnitř našeho hlavního souboru index.ts zkontrolujeme, zda máme k dispozici ID uživatele, pokud to bude pravda, přesměrujeme se na cestu /home.

//routes/auth/register.tsx
import { useState } from 'react';
import { Layout } from '~/layout/layout';
import { Link, useActionData } from '@remix-run/react';
import { ActionFunction, LoaderFunction, redirect } from '@remix-run/node';
import { registerUser, getUser } from '~/utils/auth.server';

export const loader: LoaderFunction = async ({ request }) => {
  // If user has active session, redirect to the homepage
  return (await getUser(request)) ? redirect('/') : null;
};

export const action: ActionFunction = async ({ request }) => {
  const form = await request.formData();
  const email = form.get('email');
  const password = form.get('password');
  const fullName = form.get('fullName');

  if (!email || !password || !fullName) {
    return {
      status: 400,
      body: 'Please provide email and password',
    };
  }

  if (
    typeof email !== 'string' ||
    typeof password !== 'string' ||
    typeof fullName !== 'string'
  ) {
    throw new Error(`Form not submitted correctly.`);
  }

  const allFields = { email, password, fullName };
  const user = await registerUser(allFields);
  return user;
};

export default function Register() {
  const actionData = useActionData();
  const [formError, setFormError] = useState(actionData?.error || '');

  return (
    <>
      <Layout>
        <div className="min-h-full flex items-center justify-center mt-[30vh]">
          <div className="max-w-md w-full space-y-8">
            <div>
              <span className="text-center text-slate-400 block">
                Welcome fellas!
              </span>
              <h2 className="text-center text-3xl font-extrabold text-gray-900">
                Register your account
              </h2>
            </div>

            <form method="post">
              <div>
                <div>
                  <label htmlFor="email-address" className="sr-only">
                    Full name
                  </label>
                  <input
                    id="user-name"
                    name="fullName"
                    type="text"
                    autoComplete="name"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-4 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm"
                    placeholder="Full name"
                    defaultValue={actionData?.fullName}
                  />
                </div>
                <div>
                  <label htmlFor="email-address" className="sr-only">
                    Email address
                  </label>
                  <input
                    id="email-address"
                    name="email"
                    type="email"
                    autoComplete="email"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-4 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm"
                    placeholder="Email address"
                    defaultValue={actionData?.email}
                  />
                </div>
                <div>
                  <label htmlFor="password" className="sr-only">
                    Password
                  </label>
                  <input
                    id="password"
                    name="password"
                    type="password"
                    autoComplete="current-password"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-4 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm"
                    placeholder="Password"
                    defaultValue={actionData?.password}
                  />
                </div>
              </div>
              <button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 mt-5"
              >
                Register account
              </button>
              <div>
                <p className="text-sm text-center mt-5">
                  Already have an account?
                  <span className="underline pl-1 text-green-500">
                    <Link to="/auth/login">Login</Link>
                  </span>
                </p>
              </div>
              <div className="text-xs font-semibold text-center tracking-wide text-red-500 w-full">
                {formError}
              </div>
            </form>
          </div>
        </div>
      </Layout>
    </>
  );
}

Přihlášení uživatelé

Vytvořme také funkci, která bude přihlašovat nové uživatele do naší aplikace.

export async function loginUser({ email, password }: LoginForm) {
  const user = await prisma.user.findUnique({
    where: { email },
  });

  if (!user || !(await bcrypt.compare(password, user.password))) {
    return json({ error: `Incorrect login` }, { status: 400 });
  }

  //redirect to homepage if user created
  return createUserSession(user.id, '/');
}

Tato funkce se dotáže naší databáze a vyhledá e-mail, který jsme předali jako parametr, zda neexistuje žádný e-mail a heslo neodpovídají, přesměrujeme na hlavní trasu.

Přidání směrování

Je čas, abychom nyní mohli vytvořit všechny potřebné trasy v naší celkové aplikaci. Vytvoříme pár tras, abychom mohli přidat nějakou chráněnou cestu a přesměrovat, když nemáme nastavené cookie. Směrování uvnitř Remixu funguje stejně, jako by fungovalo s aplikacemi Next nebo Nuxt (SSR).

Zaregistrujte trasu

//routes/auth/register.tsx
import { useState } from 'react';
import { Layout } from '~/layout/layout';
import { Link, useActionData } from '@remix-run/react';
import { ActionFunction, LoaderFunction, redirect } from '@remix-run/node';
import { registerUser, getUser } from '~/utils/auth.server';

export const loader: LoaderFunction = async ({ request }) => {
  // If user has active session, redirect to the homepage
  return (await getUser(request)) ? redirect('/') : null;
};

export const action: ActionFunction = async ({ request }) => {
  const form = await request.formData();
  const email = form.get('email');
  const password = form.get('password');
  const fullName = form.get('fullName');

  if (!email || !password || !fullName) {
    return {
      status: 400,
      body: 'Please provide email and password',
    };
  }

  if (
    typeof email !== 'string' ||
    typeof password !== 'string' ||
    typeof fullName !== 'string'
  ) {
    throw new Error(`Form not submitted correctly.`);
  }

  const allFields = { email, password, fullName };
  const user = await registerUser(allFields);
  return user;
};

export default function Register() {
  const actionData = useActionData();
  const [formError, setFormError] = useState(actionData?.error || '');

  return (
    <>
      <Layout>
        <div className="min-h-full flex items-center justify-center mt-[30vh]">
          <div className="max-w-md w-full space-y-8">
            <div>
              <span className="text-center text-slate-400 block">
                Welcome fellas!
              </span>
              <h2 className="text-center text-3xl font-extrabold text-gray-900">
                Register your account
              </h2>
            </div>

            <form method="post">
              <div>
                <div>
                  <label htmlFor="email-address" className="sr-only">
                    Full name
                  </label>
                  <input
                    id="user-name"
                    name="fullName"
                    type="text"
                    autoComplete="name"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-4 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm"
                    placeholder="Full name"
                    defaultValue={actionData?.fullName}
                  />
                </div>
                <div>
                  <label htmlFor="email-address" className="sr-only">
                    Email address
                  </label>
                  <input
                    id="email-address"
                    name="email"
                    type="email"
                    autoComplete="email"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-4 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm"
                    placeholder="Email address"
                    defaultValue={actionData?.email}
                  />
                </div>
                <div>
                  <label htmlFor="password" className="sr-only">
                    Password
                  </label>
                  <input
                    id="password"
                    name="password"
                    type="password"
                    autoComplete="current-password"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-4 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm"
                    placeholder="Password"
                    defaultValue={actionData?.password}
                  />
                </div>
              </div>
              <button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 mt-5"
              >
                Register account
              </button>
              <div>
                <p className="text-sm text-center mt-5">
                  Already have an account?
                  <span className="underline pl-1 text-green-500">
                    <Link to="/auth/login">Login</Link>
                  </span>
                </p>
              </div>
              <div className="text-xs font-semibold text-center tracking-wide text-red-500 w-full">
                {formError}
              </div>
            </form>
          </div>
        </div>
      </Layout>
    </>
  );
}

Cesta přihlášení

import { useState } from 'react';
import { Layout } from '~/layout/layout';
import { useActionData, Link } from '@remix-run/react';
import { ActionFunction, LoaderFunction, redirect } from '@remix-run/node';
import { loginUser, getUser } from '~/utils/auth.server';

export const loader: LoaderFunction = async ({ request }) => {
  // If user has active session, redirect to the homepage
  return (await getUser(request)) ? redirect('/') : null;
};

export const action: ActionFunction = async ({ request }) => {
  const form = await request.formData();
  const email = form.get('email')?.toString();
  const password = form.get('password')?.toString();

  if (!email || !password)
    return {
      status: 400,
      body: 'Please provide email and password',
    };

  const user = await loginUser({ email, password });
  return user;
};

export default function Login() {
  const actionData = useActionData();
  const [formError, setFormError] = useState(actionData?.error || '');

  return (
    <>
      <Layout>
        <div className="min-h-full flex items-center justify-center mt-[30vh]">
          <div className="max-w-md w-full space-y-8">
            <div>
              <span className="text-center text-slate-400 block">
                Welcome back!
              </span>
              <h2 className="text-center text-3xl font-extrabold text-gray-900">
                Log in to your account
              </h2>
            </div>
            <form className="mt-8 space-y-6" action="#" method="POST">
              <input type="hidden" name="remember" value="true" />
              <div className="rounded-md shadow-sm -space-y-px">
                <div>
                  <label htmlFor="email-address" className="sr-only">
                    Email address
                  </label>
                  <input
                    id="email-address"
                    name="email"
                    type="email"
                    autoComplete="email"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-4 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm"
                    placeholder="Email address"
                    defaultValue={actionData?.email}
                  />
                </div>
                <div>
                  <label htmlFor="password" className="sr-only">
                    Password
                  </label>
                  <input
                    id="password"
                    name="password"
                    type="password"
                    autoComplete="current-password"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-4 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm"
                    placeholder="Password"
                    defaultValue={actionData?.password}
                  />
                </div>
              </div>

              <div>
                <button
                  type="submit"
                  className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                >
                  Log in
                </button>
              </div>
              <div>
                <p className="text-sm text-center">
                  I dont have an account?
                  <span className="underline pl-1 text-green-500">
                    <Link to="/auth/register">Register</Link>
                  </span>
                </p>
              </div>
              <div className="text-xs font-semibold text-center tracking-wide text-red-500 w-full">
                {formError}
              </div>
            </form>
          </div>
        </div>
      </Layout>
    </>
  );
}

Do této chvíle jsme připraveni otestovat naši implementaci úložiště relací pro naše uživatele. To by mělo fungovat podle očekávání a vytvořit novou relaci pro přihlášené uživatele a také novou relaci pro čerstvého registrovaného uživatele.

Stránka přihlášení

Vytvoříme přihlášenou stránku, kde uživatelé uvidí své aktuálně přihlášené uživatelské jméno a e-mail s vřelou uvítací zprávou.

//routes/home.tsx
import {
  ActionFunction,
  LoaderFunction,
  redirect,
  json,
} from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { getUser } from '~/utils/auth.server';
import { logout } from '~/utils/auth.server';
import { Layout } from '~/layout/layout';

export const loader: LoaderFunction = async ({ request }) => {
  // If user has active session, redirect to the homepage
  const userSession = await getUser(request);
  if (userSession === null || undefined) return redirect('/auth/login');
  return json({ userSession });
};

export const action: ActionFunction = async ({ request }) => {
  return logout(request);
};

export default function Index() {
  const { userSession } = useLoaderData();
  const userName = userSession?.profile.fullName;
  const userEmail = userSession?.email;

  return (
    <>
      <Layout>
        <div className="text-center m-[30vh] block">
          <div>
            <small className="text-slate-400 pb-5 block">You are Logged!</small>
            <h1 className="text-4xl text-green-600 font-bold pb-3">
              Welcome to Remix Application
            </h1>
            <p className="text-slate-400">
              Name: {userName}, Email: {userEmail}
            </p>
          </div>
          <div className="text-sm mt-[40px]">
            <form action="/auth/logout" method="POST">
              <button
                name="_action"
                value="delete"
                className="font-medium text-red-600 hover:text-red-500"
              >
                Log me out
              </button>
            </form>
          </div>
        </div>
      </Layout>
    </>
  );
}

Odhlásit uživatele

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

Použili jsme metodu storage.destroy, kterou nám Remix poskytl k odstranění relace uložené v našich prohlížečích. Musíme také vytvořit vyhrazený soubor, který nás přesměruje na tuto trasu a odstraní uloženou relaci.

//route/auth/logout.tsx
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { logout } from "~/utils/auth.server";

export const action: ActionFunction = async ({ request }) => logout(request);
export const loader: LoaderFunction = async () => redirect("/");

Závěr

Úspěšně jsme vytvořili naši autentizaci pomocí Remix, MongoDB, Prisma, Tailwind s Typescript. Přestože je Remix čerstvý, stále se rozvíjející framework, oproti jiným existujícím podobným frameworkům máme spoustu výhod. Díky tomu se stal jedním z oblíbených rámců, na kterých lze pracovat v moderním vývoji.

Stránky se spoustou dynamického obsahu by z Remixu těžily, protože je ideální pro aplikace zahrnující databáze, dynamická data, uživatelské účty se soukromými daty atd. Stále je toho mnohem více, co můžeme implementovat pomocí výkonných funkcí, které nám poskytujeme. Právě jsme poškrábali povrch, více o remixu se můžete dozvědět v jejich oficiální dokumentaci zde.

Zdrojový kód tohoto článku najdete v odkazu github zde.

Hodně štěstí při kódování!