25
loading...
This website collects cookies to deliver better user experience
utils
in your (/app
) directory/app/utils
called firebase.js
npm install firebase
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth"
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyDhab0G2GmrgosEngBHRudaOhSrlr2d8_4",
authDomain: "remix-auth-tutorial.firebaseapp.com",
projectId: "remix-auth-tutorial",
storageBucket: "remix-auth-tutorial.appspot.com",
messagingSenderId: "496814875666",
appId: "1:496814875666:web:99246a28f282e9c3f2db5b"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app)
export { auth }
npx create-remix@latest
bootstrap page. index.jsx
file located in (./app/routes/index.jsx
)export default function Index() {
return (
<div className="remix__page">
<main>
<h2>Welcome to Remix Blog Auth Tutorial!</h2>
<h3>This blog was created by <strong>Chris Benjamin</strong></h3>
<p>This tutorial will show you how firebase authentication functionality works in Remix</p>
</main>
<aside>
<h3>Tutorial Links</h3>
<ul>
<li><a href="https://github.com/cbenjamin2009/remix-blog-firebase-auth" target="_blank">Github</a></li>
<li><a href="https://dev.to/chrisbenjamin" target="_blank">Tutorial</a></li>
</ul>
</aside>
</div>
);
}
sessions.server.jsx
in the root of your (/app/sessions.server.jsx
)fb:token
for firebase token. // app/sessions.js
import { createCookieSessionStorage } from "remix";
const { getSession, commitSession, destroySession } =
createCookieSessionStorage({
// a Cookie from `createCookie` or the CookieOptions to create one
cookie: {
//firebase token
name: "fb:token",
// all of these are optional
expires: new Date(Date.now() + 600),
httpOnly: true,
maxAge: 600,
path: "/",
sameSite: "lax",
secrets: ["tacos"],
secure: true
}
});
export { getSession, commitSession, destroySession };
./app/routes/login.jsx
import { auth } from "~/utils/firebase"
import { signInWithEmailAndPassword } from "@firebase/auth";
import { redirect, Form, Link, json, useActionData } from "remix";
import { getSession, commitSession } from "~/sessions.server";
import authStyles from "~/styles/auth.css";
//create a stylesheet ref for the auth.css file
export let links = () => {
return [{rel: "stylesheet", href: authStyles}]
}
// use loader to check for existing session, if found, send the user to the blogs site
export async function loader({ request }) {
const session = await getSession(
request.headers.get("Cookie")
);
if (session.has("access_token")) {
// Redirect to the blog page if they are already signed in.
// console.log('user has existing cookie')
return redirect("/blogs");
}
const data = { error: session.get("error") };
return json(data, {
headers: {
"Set-Cookie": await commitSession(session)
}
});
}
// our action function will be launched when the submit button is clicked
// this will sign in our firebase user and create our session and cookie using user.getIDToken()
export let action = async ({ request }) => {
let formData = await request.formData();
let email = formData.get("email");
let password = formData.get("password")
const {user, error} = await signInWithEmailAndPassword(auth, email, password)
// if signin was successful then we have a user
if ( user ) {
// let's setup the session and cookie wth users idToken
let session = await getSession(request.headers.get("Cookie"))
session.set("access_token", await user.getIdToken())
// let's send the user to the main page after login
return redirect("/admin", {
headers: {
"Set-Cookie": await commitSession(session),
}
})
}
return { user, error}
}
export default function Login(){
// to use our actionData error in our form, we need to pull in our action data
const actionData = useActionData();
return(
<div className="loginContainer">
<div className="authTitle">
<h1>Login</h1>
</div>
<Form method="post">
<label htmlFor="email">Email</label>
<input className="loginInput" type="email" name="email" placeholder="[email protected]" required />
<label htmlFor="password">Password</label>
<input className="loginInput" type="password" name="password" required />
<button className="loginButton" type="submit">Login</button>
</Form>
<div className="additionalLinks">
<Link to="/auth/register">Register</Link>
<Link to="/auth/forgot">Forgot Password?</Link>
</div>
<div className="errors">
{actionData?.error ? actionData?.error?.message: null}
</div>
</div>
)
}
auth
under (/app/routes/auth/
)register.jsx
in (/app/routes/auth/register.jsx
)
import { auth } from "~/utils/firebase"
import { createUserWithEmailAndPassword } from "@firebase/auth";
import { redirect, Form, useActionData, Link, json } from "remix";
import { getSession, commitSession } from "~/sessions.server";
import authStyles from "~/styles/auth.css";
//create a stylesheet ref for the auth.css file
export let links = () => {
return [{rel: "stylesheet", href: authStyles}]
}
// This will be the same as our Sign In but it will say Register and use createUser instead of signIn
export let action = async ({ request }) => {
let formData = await request.formData();
let email = formData.get("email");
let password = formData.get("password")
//perform a signout to clear any active sessions
await auth.signOut();
//setup user data
let {session: sessionData, user, error: signUpError} = await createUserWithEmailAndPassword(auth, email, password)
if (!signUpError){
let session = await getSession(request.headers.get("Cookie"))
session.set("access_token", auth.currentUser.access_token)
return redirect("/blogs",{
headers: {
"Set-Cookie": await commitSession(session),
}
})
}
// perform firebase register
return {user, signUpError}
}
export default function Register(){
const actionData = useActionData();
return(
<div className="loginContainer">
<div className="authTitle">
<h1>Register</h1>
</div>
<Form method="post">
<label htmlFor="email">Email</label>
<input className="loginInput" type="email" name="email" placeholder="[email protected]" required />
<label htmlFor="password">Password</label>
<input className="loginInput" type="password" name="password" required />
<button className="loginButton" type="submit">Register</button>
</Form>
<div className="additionalLinks">
Already Registered? <Link to="/login">Login</Link>
</div>
<div className="errors">
{actionData?.error ? actionData?.error?.message: null}
</div>
</div>
)
}
<Form>
tag to call an action and post the request which updates correctly. Root.jsx
file as followsimport { redirect } from "remix";
import { getSession } from "~/sessions.server";
import { destroySession } from "~/sessions.server";
import { auth } from "~/utils/firebase";
// loader function to check for existing user based on session cookie
// this is used to change the nav rendered on the page and the greeting.
export async function loader({ request }) {
const session = await getSession(
request.headers.get("Cookie")
);
if (session.has("access_token")) {
const data = { user: auth.currentUser, error: session.get("error") };
return json(data, {
headers: {
"Set-Cookie": await commitSession(session)
}
});
} else {
return null;
}
}
forgot.jsx
under (/app/routes/auth/forgot.jsx
)forgot.jsx
:import { auth } from "~/utils/firebase"
import { sendPasswordResetEmail } from "@firebase/auth";
import { redirect, Form, Link } from "remix";
export let action = async ({ request }) => {
// pull in the form data from the request after the form is submitted
let formData = await request.formData();
let email = formData.get("email");
// perform firebase send password reset email
try{
await sendPasswordResetEmail(auth, email)
}
catch (err) {
console.log("Error: ", err.message)
}
// success, send user to /login page
return redirect('/login')
}
export default function Login(){
return(
<div className="loginContainer">
<div className="authTitle">
<h1>Forgot Password?</h1>
</div>
<Form method="post">
<p>Enter the email address associated with your account</p>
<input className="loginInput" type="email" name="email" placeholder="[email protected]" required />
<button className="loginButton" type="submit">Submit</button>
</Form>
<div className="additionalLinks">
Not Yet Registered? <Link to="/auth/register">Register</Link>
</div>
</div>
)
}
root.jsx
import {auth} from "~/utils/firebase"
import { useLoaderData, json } from "remix";
import { getSession } from "./sessions.server";
import { commitSession } from "./sessions.server";
// loader function to check for existing user based on session cookie
// this is used to change the nav rendered on the page and the greeting.
export async function loader({ request }) {
const session = await getSession(
request.headers.get("Cookie")
);
if (session.has("access_token")) {
const data = { user: auth.currentUser, error: session.get("error") };
return json(data, {
headers: {
"Set-Cookie": await commitSession(session)
}
});
} else {
return null;
}
}
root.jsx
. <Form>
tag which will allow our action loader to run when the user clicks our logout button and not trigger a full page refresh. We are also going to add a Class so we can update our styles to make it match the rest of the nav.function Layout({ children }) {
// let's grab our loader data to see if it's a sessioned user
let data = useLoaderData();
// let's check to see if we have a user, if so we will use it to update the greeting and link logic for Login/Logout in Nav
let loggedIn = data?.user
return (
<div className="remix-app">
<header className="remix-app__header">
<div className="container remix-app__header-content">
<Link to="/" title="Remix" className="remix-app__header-home-link">
<RemixLogo />
</Link>
<nav aria-label="Main navigation" className="remix-app__header-nav">
<ul>
<li>
<Link to="/">Home</Link>
</li>
{!loggedIn ? <li>
<Link to="/login">Login</Link>
</li> :
<li>
<Form method="post">
<button type="submit" className="navLogoutButton">Logout</button>
</Form>
</li> }
<li>
<Link to="/blogs">Blogs</Link>
</li>
<li>
<a href="https://remix.run/docs">Remix Docs</a>
</li>
<li>
<a href="https://github.com/remix-run/remix">GitHub</a>
</li>
</ul>
</nav>
</div>
</header>
<div className="remix-app__main">
<div className="container remix-app__main-content">{children}</div>
</div>
<footer className="remix-app__footer">
<div className="container remix-app__footer-content">
<p>© You!</p>
</div>
</footer>
</div>
);
}
global.css
from (/app/styles/global.css
) and update the exiting a tags and adding .navLogoutButton styling as follows:a, .navLogoutButton {
color: var(--color-links);
text-decoration: none;
}
a:hover, .navLogoutButton:hover {
color: var(--color-links-hover);
text-decoration: underline;
}
.navLogoutButton{
background: none;
border: none;
font-family: var(--font-body);
font-weight: bold;
font-size: 16px;
}
index.jsx
from (/app/routes/blogs/index.jsx
)// our Posts function which will return the rendered component on the page .
export default function Posts() {
let posts = useLoaderData();
return (
<div>
<h1>My Remix Blog</h1>
<p>Click on the post name to read the post</p>
<div>
<Link to="/admin">Blog Admin (Edit/Create)</Link>
</div>
<ul>
{posts.map(post => (
<li className="postList" key={post.slug}>
<Link className="postTitle" to={post.slug}>{post.title}</Link>
</li>
))}
</ul>
</div>
)
}
index.jsx
under (/app/index.jsx
)import { useLoaderData, json, Link, redirect} from "remix";
import { auth } from "~/utils/firebase"
import { getSession } from "~/sessions.server";
import { destroySession, commitSession } from "~/sessions.server";
// use loader to check for existing session
export async function loader({ request }) {
const session = await getSession(
request.headers.get("Cookie")
);
if (session.has("access_token")) {
//user is logged in
const data = { user: auth.currentUser, error: session.get("error") };
return json(data, {
headers: {
"Set-Cookie": await commitSession(session)
}
});
}
// user is not logged in
return null;
}
npm run dev
to get it going. Click on Login in the top Nav of your remix application and then click Register link next to Not Yet Registered. /app/styles/auth.css
)/* Used for styling the Auth pages (Login, Register, Forgot) */
.loginContainer{
margin-top: 1em;
display: flex;
flex-direction: column;
text-align: center;
padding: 3em;
background-color: rgba(0, 0, 0, 0.301);
border-radius: 10px;
}
.loginContainer h1 {
margin-bottom: 2em;
}
.loginContainer label {
font-size: 1.5em;
font-weight: bold;
}
.loginContainer form {
display: flex;
flex-direction: column;
}
.loginInput{
padding: .5em;
font-size: 1.5em;
margin: 1em;
border-radius: 1em;
border: none;
}
.loginButton {
padding: .2em;
font-size: 2em;
border-radius: 1em;
width: 200px;
margin-left: auto;
margin-right: auto;
margin-bottom: 2em;
cursor: pointer;
background-color: rgba(47, 120, 255, 0.733);
}
.loginButton:hover{
border: 2px dashed skyblue;
background-color: rgba(47, 120, 255, 0.9);
}
.additionalLinks{
display: flex;
justify-content: space-evenly;
font-size: x-large;
}
login.jsx, forgot.jsx, register.jsx
import authStyles from "~/styles/auth.css";
//create a stylesheet ref for the auth.css file
export let links = () => {
return [{rel: "stylesheet", href: authStyles}]
}
admin.jsx
from (/app/routes/admin.jsx
)import { Outlet, Link, useLoaderData, redirect, json } from 'remix';
import { getPosts } from "~/post";
import adminStyles from "~/styles/admin.css";
import { getSession } from '~/sessions.server';
import { commitSession } from '~/sessions.server';
//create a stylesheet ref for the admin.css file
export let links = () => {
return [{rel: "stylesheet", href: adminStyles}]
}
// this is the same loader function from our Blogs page
// check for existing user, if not then redirect to login, otherwise set cookie and getPosts()
export async function loader({ request }) {
const session = await getSession(
request.headers.get("Cookie")
);
if (!session.has("access_token")) {
return redirect("/login");
}
const data = { error: session.get("error") };
return json(data, {
headers: {
"Set-Cookie": await commitSession(session)
}
}), getPosts();
}
$edit.jsx
file from (/app/routes/admin/$edit.jsx
)export default function PostSlug() {
let errors = useActionData();
let transition = useTransition();
let post = useLoaderData();
return (
<>
<Form method="post">
<p>
<input className="hiddenBlogID" name="id" defaultValue={post.id}>
</input>
</p>
<p>
<label htmlFor="">
Post Title: {" "} {errors?.title && <em>Title is required</em>} <input type="text" name="title" defaultValue={post.title}/>
</label>
</p>
<p>
<label htmlFor=""> Post Slug: {" "} {errors?.slug && <em>Slug is required</em>}
<input defaultValue={post.slug} id="slugInput" type="text" name="slug"/>
</label>
</p>
<p>
<label htmlFor="markdown">Markdown:</label>{" "} {errors?.markdown && <em>Markdown is required</em>}
<br />
<textarea defaultValue={post.markdown} name="markdown" id="" rows={20} cols={50}/>
</p>
<p>
<button type="submit" className="adminButton updateButton">{transition.submission ? "Updating..." : "Update Post"}</button>
</p>
</Form>
<Form method="delete">
<p>
<input className="hiddenBlogID" name="id" defaultValue={post.id}>
</input>
</p>
<p>
<button className="adminButton deleteButton" type="submit">Delete</button>
</p>
</Form>
</>
)
}
export let action = async ({ request }) => {
let formData = await request.formData();
let title = formData.get("title");
let slug = formData.get("slug")
let markdown = formData.get("markdown")
let id = formData.get("id");
if (request.method == 'DELETE'){
await deletePost(id)
return redirect("/admin")
}
let errors = {};
if (!title) errors.title = true;
if (!slug) errors.slug = true;
if (!markdown) errors.markdown = true;
if (Object.keys(errors).length) {
return errors;
}
await updatePost({id, title, slug, markdown});
return redirect("/admin")
}
post.js
file from (/app/post.js
)export async function deletePost(post){
await prisma.$connect()
await prisma.posts.delete({
where: {
id: post
},
})
prisma.$disconnect();
return(post);
}
$edit.jsx
to bring in this deletePost() function. $edit.jsx
and update the import at the top import { getPostEdit, updatePost, deletePost } from "~/post";
admin.css
from (/app/styles/admin.css
).admin {
display: flex;
flex-direction: row;
}
.admin > h1 {
padding-right: 2em;
}
.admin > nav {
flex: 1;
border-left: solid 2px #555;
padding-left: 2em;
}
.hiddenBlogID {
display: none;
}
.adminNewPostButton{
margin-top: 2em;
background-color: royalblue;
color: white;
border-radius: 10px;
padding: 1em;
}
.adminTitle {
font-size: x-large;
color: crimson;
}
.remix-app__header{
background-color: rgb(141, 20, 20);
}
.adminButton{
color: white;
padding: 1em;
border-radius: 2em;
}
.deleteButton{
background-color: rgb(158, 3, 3);
}
.updateButton{
background-color: rgb(2, 5, 180);
}
npm run build
vercel deploy
🚀🚀🚀