From 5342c27c19c43dd2abcf550bd53eb7dcd53398f4 Mon Sep 17 00:00:00 2001 From: pilinux Date: Fri, 1 Sep 2023 12:14:57 +0200 Subject: [PATCH] example: login logout process --- app/routes/_index.jsx | 8 ++ app/routes/login/index.jsx | 238 ++++++++++++++++++++++++++++++++++++ app/routes/logout/index.jsx | 56 +++++++++ 3 files changed, 302 insertions(+) create mode 100644 app/routes/login/index.jsx create mode 100644 app/routes/logout/index.jsx diff --git a/app/routes/_index.jsx b/app/routes/_index.jsx index f732b67..9ebac3e 100644 --- a/app/routes/_index.jsx +++ b/app/routes/_index.jsx @@ -51,6 +51,7 @@ export default function Index() { Remix Docs +
  • Example: Set Session
  • @@ -63,9 +64,16 @@ export default function Index() {
  • Example: Delete Cookie
  • +
  • Remote API Status [GET]
  • +
  • + Login [POST] +
  • +
  • + Logout [POST] +
  • diff --git a/app/routes/login/index.jsx b/app/routes/login/index.jsx new file mode 100644 index 0000000..0ef23bc --- /dev/null +++ b/app/routes/login/index.jsx @@ -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 ( +
    +
    +
    +

    Login

    + +
    +
    + + + {emailError &&

    {emailError}

    } +
    +
    + + + {passwordError &&

    {passwordError}

    } +
    +
    + +
    + {authError &&

    {authError}

    } + {serverError &&

    {serverError}

    } +
    + +
    + Home +
    +
    +
    + ); +} diff --git a/app/routes/logout/index.jsx b/app/routes/logout/index.jsx new file mode 100644 index 0000000..a48c6ff --- /dev/null +++ b/app/routes/logout/index.jsx @@ -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 ( +
    +
    +
    +

    Logout?

    +
    +
    +
    + +
    +
    +
    + +
    + Home +
    +
    + ); +}