Skip to content

Commit

Permalink
example: login logout process
Browse files Browse the repository at this point in the history
  • Loading branch information
pilinux committed Sep 1, 2023
1 parent fe0192e commit 5342c27
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 0 deletions.
8 changes: 8 additions & 0 deletions app/routes/_index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default function Index() {
Remix Docs
</a>
</li>

<li>
<Link to="session-set">Example: Set Session</Link>
</li>
Expand All @@ -63,9 +64,16 @@ export default function Index() {
<li>
<Link to="cookie-delete">Example: Delete Cookie</Link>
</li>

<li>
<Link to="status">Remote API Status [GET]</Link>
</li>
<li>
<Link to="login">Login [POST]</Link>
</li>
<li>
<Link to="logout">Logout [POST]</Link>
</li>
</ul>
</div>
</div>
Expand Down
238 changes: 238 additions & 0 deletions app/routes/login/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// example: login action
import { json } from "@remix-run/node";
import { Form, Link } from "@remix-run/react";
import { useState } from "react";

import { remoteApi } from "~/api.server";
import { sha512, sha3_512 } from "~/crypto";
import { jwtExpiry } from "~/jwt";
import { cookieHandler } from "~/cookies.server";

function validateEmail(email) {
const regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
return regex.test(email);
}

function validatePassword(password) {
// ,;.:-_#'+*@<>!"§$|%&/()[]=?{}\
const regex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[,;.:\-\_#\'+*@<>!"§$|%&/\(\)\[\]=?\{\}\\]).{6,}$/;
return regex.test(password);
}

// https://remix.run/docs/en/main/guides/resource-routes
// on server-side
export const action = async ({ request }) => {
const routePart = new URL(request.url).pathname;

const form = await request.formData();
const email = form.get("email");
const sha2Password = form.get("password");

// on server-side, hash the password again
const sha3Password = sha3_512(sha2Password);

switch (request.method) {
case "POST": {
// console.log(email);
// console.log(sha2Password);
// console.log(sha3Password);

// build JSON object
const requestData = {
email: email,
password: sha3Password,
};

// convert the object to a JSON string
const jsonBody = JSON.stringify(requestData);

// forward to remote API
try {
const url = remoteApi + "/api/v1" + routePart;

const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: jsonBody,
});

const data = await res.json();
// console.log(data);
// console.log(res.status);
// console.log(res.statusText);

// authentication successful
if (res.status === 200) {
// handle JWT
const { accessJWT, refreshJWT } = data;
const expAccessJWT = jwtExpiry(accessJWT);
const expRefreshJWT = jwtExpiry(refreshJWT);
// console.log(accessJWT);
// console.log(refreshJWT);
// console.log(expAccessJWT);
// console.log(expRefreshJWT);

// construct HttpOnly cookie
const cookieHeader = request.headers.get("Cookie");

// JWT Access
const userPrefs1 = cookieHandler("__access");
const cookie1 = (await userPrefs1.parse(cookieHeader)) || {};
cookie1.jwt = accessJWT;

// JWT Refresh
const userPrefs2 = cookieHandler("__refresh");
const cookie2 = (await userPrefs2.parse(cookieHeader)) || {};
cookie2.jwt = refreshJWT;

let headers = new Headers();
headers.append(
"Set-Cookie",
await userPrefs1.serialize(cookie1, {
expires: new Date(expAccessJWT),
})
);
headers.append(
"Set-Cookie",
await userPrefs2.serialize(cookie2, {
expires: new Date(expRefreshJWT),
})
);

// construct a JSON response with headers to set cookies
return json(
{ message: "ok" },
{
status: 200,
headers,
}
);
}

// authentication failed
return json({ message: data.message }, res.status);
} catch (e) {
return json({ message: "remote API down" }, 502);
}
}
}
};

// on client-side
export default function Login() {
const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState("");
const [authError, setAuthError] = useState("");
const [serverError, setServerError] = useState("");

async function handleSubmit(event) {
event.preventDefault();

const email = event.target.email.value;
const password = event.target.password.value;

const isEmailValid = validateEmail(email);
const isPasswordValid = validatePassword(password);

if (!isEmailValid) {
setEmailError("Invalid email format");
setPasswordError("");
setAuthError("");
setServerError("");
return;
}
if (!isPasswordValid) {
setEmailError("");
setPasswordError(
"Password must be at least 6 characters long and contain A-Z, a-z, 0-9, and minimum one special character"
);
setAuthError("");
setServerError("");
return;
}
setEmailError("");
setPasswordError("");
setAuthError("");
setServerError("");

// on client-side, hash the password
const sha2Password = sha512(password);

const formData = new FormData();
formData.append("email", email);
formData.append("password", sha2Password);

// send data to remix server
try {
const res = await fetch("/login", {
method: "POST",
body: formData,
});

if (res.status === 200) {
// authentication successful, perform client-side redirect
window.location.href = "/logout";
}
if (res.status !== 200 && res.status < 500) {
setAuthError("Invalid email or password");
}
if (res.status >= 500) {
setServerError("Remote server error");
}
} catch (e) {
setServerError("Internal server error");
}
}

return (
<div className="container-sm">
<div className="row">
<div className="col-sm-4">
<h2>Login</h2>

<Form method="post" onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="email-input" className="form-label">
Email
</label>
<input
id="email-input"
name="email"
type="email"
className="form-control"
placeholder="name@email.com"
/>
{emailError && <p className="text-danger">{emailError}</p>}
</div>
<div className="mb-3">
<label htmlFor="password-input" className="form-label">
Password
</label>
<input
id="password-input"
name="password"
type="password"
className="form-control"
placeholder="password"
/>
{passwordError && <p className="text-danger">{passwordError}</p>}
</div>
<div className="mb-2">
<button type="submit" className="btn btn-primary">
Submit
</button>
</div>
{authError && <p className="text-danger">{authError}</p>}
{serverError && <p className="text-danger">{serverError}</p>}
</Form>

<hr />
<Link to="/">Home</Link>
</div>
</div>
</div>
);
}
56 changes: 56 additions & 0 deletions app/routes/logout/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// // example: logout action
import { Form, Link } from "@remix-run/react";
import { redirect } from "@remix-run/node";

import { cookieHandler } from "~/cookies.server";

export const action = async ({ request }) => {
let headers = new Headers();
const cookieHeader = request.headers.get("Cookie");

// JWT Access
const userPrefs1 = cookieHandler("__access");
const cookie1 = (await userPrefs1.parse(cookieHeader)) || {};
if (Object.keys(cookie1).length !== 0) {
headers.append(
"Set-Cookie",
await userPrefs1.serialize(cookie1, { maxAge: -1 })
);
}

// JWT Refresh
const userPrefs2 = cookieHandler("__refresh");
const cookie2 = (await userPrefs2.parse(cookieHeader)) || {};
if (Object.keys(cookie2).length !== 0) {
headers.append(
"Set-Cookie",
await userPrefs2.serialize(cookie2, { maxAge: -1 })
);
}

return redirect("/login", { headers });
};

// on client-side
export default function Logout() {
return (
<div className="container-sm">
<div className="row">
<div className="col-sm-4"></div>
<h2>Logout?</h2>
<div className="mb-3">
<Form method="post">
<div className="mb-2">
<button type="submit" className="btn btn-danger">
Logout
</button>
</div>
</Form>
</div>

<hr />
<Link to="/">Home</Link>
</div>
</div>
);
}

0 comments on commit 5342c27

Please sign in to comment.