Authentication: Register & Login Upload form data with file in Remix & Mysql
In the previous tutorial, you learnt to do use Prisma to synchronize models with MYSQL database schema and create a migration to add a new column to users table.
In this tutorial, you learn how to create authentication system in Remix. A visitor registers an user account using register form. In the register form, the user can upload an profile image and other information such as first name, last name, username, email, and password. Upon successful registration, the user account is saved in MYSQL database. The password is encrypted using bcryptjs.
A registered user logins to the app by providing username and password. A successful login will create a session for that user. To access a restricted API, a user has to be authenticated. In a restricted component, the user id is read from the session. If there is a valid user id in the session, the user is allowed to access the restricted API. Otherwise, it will be redirected to the login page.
Run the following command to install bscryptjs:
npm install bcryptjs
User Registration
In the app folder, create utils folder. In the utils, create db.tsx file to define a prisma client object that act as a repository. The repository is used to manage user data in MYSQL database.
utils/dt.tsx
import { PrismaClient } from "@prisma/client";
import bcrypt from 'bcryptjs'; import { redirect } from "@remix-run/node"; import { json, unstable_createFileUploadHandler, unstable_parseMultipartFormData, unstable_composeUploadHandlers, unstable_createMemoryUploadHandler } from "@remix-run/node"; import {db} from "../../utils/db"; import { Form, useActionData, useTransition, } from "@remix-run/react"; export const meta = () => { const title = 'Register'; const description = 'Here is my Awesome Register page!'; return { charset: 'utf-8', title, description, keywords: 'Remix,register, create user, Mysql', }; }; export async function action({ request}) { const validateEmail = (email) => { if (!email) { return "Email is Required!"; } else if (!(/\S+@\S+\.\S+/.test(email))) { return "Invalid emaill!"; } }; const validateUsername = (username) => { if (!username) { return "Username is Required!"; } else if(!( /^[A-Za-z]+/.test(username))){ return "Username must start with a letter!"; } }; const validatePassword = (password) => { if (!password) { return "Password is Required!"; } else if(!( /^[A-Za-z]\w{8,16}$/.test(password))) { return "Weak password! A strong password should contain characters, numeric digits, underscore and first character must be a letter."; } }; const uploadHandler_ = unstable_composeUploadHandlers( // parse file unstable_createFileUploadHandler({ maxPartSize: 5_000_000, directory: "public/uploads", file: ({ filename }) => filename, }), // parse other form data into memory unstable_createMemoryUploadHandler() ); const formData = await unstable_parseMultipartFormData( request, uploadHandler_ ); const file = formData.get("photo"); const fname= formData.get("firstname"); const lname= formData.get("lastname"); const uname= formData.get("username"); const email= formData.get("email"); let password= formData.get("password"); const formErrors = { email: validateEmail(email), username: validateUsername(uname), password: validatePassword(password) }; //if there are errors, we return the form errors if (Object.values(formErrors).some(Boolean)) return { formErrors }; // generate enctypted password password= await bcrypt.hash(password, 10); const user=await db.users.create( { data:{ firstName:fname, lastName:lname, username:uname, email:email, password:password, photo:file?file.name:'', createdAt: new Date(), updatedAt: new Date() }} ); if(user){ console.log("create user:",user); return redirect('/user/login'); } else{ console.log("err:","failed to create the user"); } if (file) { console.log(`File uploaded to public/uploads/${file.name}`); } else { console.log("No file uploaded",formData); } return {}; } // Note the "action" export name, this will handle our form POST export default function Register() { const transition = useTransition(); const data = useActionData(); return ( <div className="grid place-items-center"> <div className="w-full max-w-xs"> <Form method="post" encType="multipart/form-data" className="bg-gray-200 shadow-md rounded px-8 pt-6 pb-8 mb-4 self-center"> <fieldset disabled={transition.state === "submitting"} > <div className="mb-4"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="firstname"> First name </label> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="firstname" name="firstname" type="text" placeholder="First name"/> </div> <div className="mb-4"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="lastname"> Last name </label> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="lastname" name="lastname" type="text" placeholder="Last name"/> </div> <div className="mb-4"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="username"> Username </label> <input className="shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" id="username" name="username" type="text" placeholder="Username"/> { data && data.formErrors && data.formErrors.username && <p className="text-red-500 text-xs italic"> {data.formErrors.username} </p> } </div> <div className="mb-4"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="email"> Email </label> <input className="shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" id="email" name="email" type="text" placeholder="Email"/> { data && data.formErrors && data.formErrors.email && <p className="text-red-500 text-xs italic"> {data.formErrors.email} </p> } </div> <div className="mb-6"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="password"> Password </label> <input className="shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" id="password" name="password" type="password" placeholder="******************"/> { data && data.formErrors && data.formErrors.password && <p className="text-red-500 text-xs italic"> {data.formErrors.password} </p> } </div> <div className="mb-6"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="photo"> Profile Photo </label> <input type="file" name="photo" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" id="photo"/> </div> <div className="flex items-center justify-between"> <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit"> {transition.state === "submitting" ? "Submitting..." : "Submit"} </button> </div> </fieldset> </Form> </div> </div> ); }
Upload Form Data with File
Form Validations
User Login & Logout
import { createCookieSessionStorage, redirect, } from "@remix-run/node"; const sessionSecret = 'sessionYsecret'; if (!sessionSecret) { throw new Error("SESSION_SECRET must be set"); } const storage = createCookieSessionStorage({ cookie: { name: "user_session", // normally you want this to be `secure: true` // but that doesn't work on localhost for Safari // https://web.dev/when-to-use-local-https/ secure: process.env.NODE_ENV === "production", secrets: [sessionSecret], sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 30, httpOnly: true, }, }); 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), }, }); } function getUserSession(request: Request) { return storage.getSession(request.headers.get("Cookie")); } export async function getUserId(request: Request) { const session = await getUserSession(request); const userId = session.get("userId"); if (!userId || typeof userId !== "number") return null; return userId; } export async function logout(request: Request) { const session = await getUserSession(request); return redirect("/user/login", { headers: { "Set-Cookie": await storage.destroySession(session), }, }); }
In the user folder, create login.jsx and logout.jsx.
import { json } from "@remix-run/node"; import {db} from "../../utils/db"; import {createUserSession} from "../../utils/session"; import bcrypt from 'bcryptjs'; import { useActionData, Form, useTransition, } from "@remix-run/react"; export const meta = () => { const title = 'Login'; const description = 'Here is my Awesome login page!'; return { charset: 'utf-8', title, description, keywords: 'Remix,Awesome, Login, Mysql', }; }; export async function action({ request}) { const body = await request.formData(); const username= body.get("username"); const password= body.get("password"); var mess=""; const user = await db.users.findFirst({ where: { username: username } }); if(user){ const isCorrectPassword = await bcrypt.compare( password, user.password ); if (!isCorrectPassword) mess="Invalid password!"; else{ return await createUserSession(user.id, '/user'); } } else{ mess="User not found"; } return json({message:mess}); } export default function Login() { const transition = useTransition(); const data = useActionData(); return ( <div className="grid place-items-center"> <div className="w-full max-w-xs"> <Form method="post" className="bg-gray-200 shadow-md rounded px-8 pt-6 pb-8 mb-4 self-center"> <fieldset disabled={transition.state === "submitting"} > <div className="mb-4"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="username"> Username </label> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" name="username" id="username" type="text" placeholder="Username"/> </div> <div className="mb-6"> <label classname="block text-gray-700 text-sm font-bold mb-2" htmlFor="password"> Password </label> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" name="password" id="password" type="password" placeholder="******************"/> </div> <div className="flex items-center justify-between"> <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit"> {transition.state === "submitting" ? "Validating..." : "Log In"} </button> <a href="/user/register" className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800"> No account? create user </a> </div> <p className="text-red-500 text-xs italic">{data ? data.message : ""}</p> </fieldset> </Form> </div> </div> ); }
logout.jsx
import { logout } from "~/utils/session"; export const loader = async ({request}) => { return logout(request); };
Comments
Post a Comment