Skip to content

Commit

Permalink
Add login with Github
Browse files Browse the repository at this point in the history
  • Loading branch information
fileformat committed Sep 24, 2024
1 parent b34ed50 commit 7a52429
Show file tree
Hide file tree
Showing 17 changed files with 401 additions and 26 deletions.
26 changes: 26 additions & 0 deletions app/components/AuthButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Link as RemixLink, useRouteLoaderData } from "@remix-run/react";
import { PiUserCircle, PiUserCircleFill } from "react-icons/pi";
import { User } from "~/types/User";

export function AuthButton() {
const user = useRouteLoaderData<User|null>("root");

let authImage:JSX.Element = <PiUserCircle title={"Not logged in"} />;
if (user) {
if (user.avatar) {
authImage = <img src={user.avatar} alt={user.displayName} className="rounded-circle" style={{"width": "1.75rem", "height": "1.75rem"}} />
} else {
authImage = <PiUserCircleFill title={user.displayName} />;
}
}

console.log('authbutton', JSON.stringify(user));
return (
<>
<RemixLink className="d-none d-sm-inline text-dark" to="/auth/" style={{ "fontSize": "1.75rem" }}>
{ authImage }
</RemixLink>
</>
)
}

8 changes: 4 additions & 4 deletions app/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ColorSchemeToggle } from "~/components/ColorSchemeToggle";

const links = [
{ link: 'https://github.com/regexplanet/regex-zone/issues', label: 'Feedback' },
//{ link: 'https://github.com/regexplanet/regex-zone/issues', label: 'Feedback' },
{ link: 'https://github.com/regexplanet/regex-zone?tab=readme-ov-file#credits', label: 'Credits'},
{ link: 'https://github.com/regexplanet/regex-zone', label: 'Source' },
{ link: 'https://github.com/regexplanet/regex-zone?tab=readme-ov-file#other-libraries-of-regex-patterns', label: 'Alternatives' },
//{ link: 'https://github.com/regexplanet/regex-zone?tab=readme-ov-file#other-libraries-of-regex-patterns', label: 'Alternatives' },
];

export function Footer() {
Expand All @@ -15,7 +15,7 @@ export function Footer() {
{link.label}
</a>);
if (index < links.length - 1) {
initial.push(<span className="mx-1" key="key{{index}}">|</span>);
initial.push(<span className="mx-1" key={`key${index}`}>|</span>);
}
}
);
Expand All @@ -27,7 +27,7 @@ export function Footer() {
{ initial }
</small>

<ColorSchemeToggle />
<ColorSchemeToggle key="cst"/>
</footer>
</>
)
Expand Down
10 changes: 6 additions & 4 deletions app/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { PiBlueprint, PiBlueprintBold, PiLink, PiMagnifyingGlass, PiMagnifyingGl
import { Link as RemixLink } from "@remix-run/react";

import RegexZoneSvg from './RegexZoneSvg';
import { NavbarLink, NavbarLinkItem } from '~/components/NavbarLink';
import { NavbarLink, NavbarLinkItem } from './NavbarLink';
import { AuthButton } from './AuthButton';

const links:NavbarLinkItem[] = [
{ link: '/patterns/', label: 'Patterns', icon: <PiBlueprint />, icon_bold: <PiBlueprintBold /> },
Expand All @@ -19,14 +20,15 @@ export function Navbar() {
return (
<>
<nav className="navbar navbar-expand bg-body-tertiary border-bottom">
<div className="container-lg">
<RemixLink className="navbar-brand fs-4 fw-bold" to="/">
<div className="container-lg d-flex">
<RemixLink className="navbar-brand fs-4 fw-bold flex-grow-1" to="/">
<RegexZoneSvg height={'2rem'} className="pe-2 d-none d-md-inline" />
Regex Zone
</RemixLink>
<ul className="navbar-nav">
<ul className="navbar-nav mt-1">
{items}
</ul>
<AuthButton />
</div>
</nav>
</>
Expand Down
41 changes: 25 additions & 16 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@ import {
} from "@remix-run/react";
import { Navbar } from "~/components/Navbar";
import { Footer } from "~/components/Footer";
import { LoaderFunctionArgs } from "@remix-run/node";

import { authenticator } from "~/services/auth.server";

export async function loader({ request }: LoaderFunctionArgs) {
return await authenticator.isAuthenticated(request);
}

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossOrigin="anonymous"
/>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossOrigin="anonymous"
/>
<Meta />
<Links />
<style>{`
Expand All @@ -35,7 +42,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<body>
<Navbar />
<div className="container-lg">
{children}
{children}
</div>
<Footer />
<ScrollRestoration />
Expand All @@ -46,21 +53,23 @@ export function Layout({ children }: { children: React.ReactNode }) {
}

export default function App() {
return <Outlet />;
return (
<Outlet />
);
}

export function ErrorBoundary() {
const error = useRouteError();
return (
<>
<h1 className="py-2">Error</h1>
<div>
{isRouteErrorResponse(error)
? `${error.status} ${error.statusText}`
: error instanceof Error
? error.message
: "Unknown Error"}
</div>
<h1 className="py-2">Error</h1>
<div>
{isRouteErrorResponse(error)
? `${error.status} ${error.statusText}`
: error instanceof Error
? error.message
: "Unknown Error"}
</div>
</>
);
}
63 changes: 63 additions & 0 deletions app/routes/auth._index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { LoaderFunctionArgs } from "@remix-run/node";
import { Link as RemixLink, useLoaderData } from "@remix-run/react";
import { authenticator } from "~/services/auth.server";
import { cookieStorage } from "~/services/session.server";

export async function loader({ request }: LoaderFunctionArgs) {
//console.log('in loader', (await cookieStorage.getSession(request.headers.get("Cookie"))).get("user"))
const session = await cookieStorage.getSession(request.headers.get("Cookie"));
const sessionUser = session.get("user");
const authUser = await authenticator.isAuthenticated(request);
return {
//
user: sessionUser,
sessionUser,
authUser,
session: (await cookieStorage.getSession(request.headers.get("Cookie")))
};
}

function LoginSection() {
return (
<>
<p>You are not logged in!</p>
<RemixLink className="btn btn-primary" to="login.html">Login</RemixLink>
</>
)
}

function LogoutSection({ user }: { user: any }) {
return (
<>
<p>You are logged in as <span className="border rounded bg-body-tertiary text-body-secondary p-2">{user.displayName} ({user.providerName}@{user.provider})</span></p>
<p>Your email is <span className="border rounded bg-body-tertiary text-body-secondary p-2">{user.email}</span></p>
<p>Your profile image is <img className="px-2" src={user.avatar} alt={user.displayName} style={{"height":"2em"}} /></p>
<form action="/auth/logout.html" method="post">
<input type="submit" className="btn btn-primary" value="Logout" />
</form>
</>
)
}

export default function AuthIndex() {
const data = useLoaderData<typeof loader>();

return (
<>
<h1 className="py-2">Authentication</h1>
{ data.user ? <LogoutSection user={data.user} /> : <LoginSection/> }
<details className="pt-3">
<summary>Raw Auth User Data</summary>
<pre>{JSON.stringify(data.user, null, 4)}</pre>
</details>
<details className="">
<summary>Raw Session User Data</summary>
<pre>{JSON.stringify(data.user, null, 4)}</pre>
</details>
<details>
<summary>Raw Session Data</summary>
<pre>{JSON.stringify(data.session, null, 4)}</pre>
</details>
</>
)
}
22 changes: 22 additions & 0 deletions app/routes/auth.github.callback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { redirect, type LoaderFunctionArgs } from "@remix-run/node";
import { authenticator } from "~/services/auth.server";
import { cookieStorage } from "~/services/session.server";

export async function loader({ request }: LoaderFunctionArgs) {
const user = await authenticator.authenticate("github", request);

console.log('user in callback', JSON.stringify(user));
if (user != null) {
const session = await cookieStorage.getSession(request.headers.get("Cookie"));
session.set("user", user);
return redirect('/auth/', { headers: { "Set-Cookie": await cookieStorage.commitSession(session) }});
}
return redirect("/auth/");

/*
return authenticator.authenticate("github", request, {
successRedirect: "/auth/sucess.html",
failureRedirect: "/auth/failure.html",
});
*/
}
14 changes: 14 additions & 0 deletions app/routes/auth.github.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { authenticator } from "~/services/auth.server";

export async function loader() {
return redirect("/auth/");
}

export async function action({ request }: ActionFunctionArgs) {
return authenticator.authenticate("github", request, {
successRedirect: "/auth/",
failureRedirect: "/auth/",
});
}
22 changes: 22 additions & 0 deletions app/routes/auth.login[.]html.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { LoaderFunctionArgs } from "@remix-run/node";
import { Link as RemixLink, useLoaderData } from "@remix-run/react";
import { PiGithubLogoBold } from "react-icons/pi";
import { authenticator } from "~/services/auth.server";

export async function loader({ request }: LoaderFunctionArgs) {
return await authenticator.isAuthenticated(request);
}

export default function AuthIndex() {
const data = useLoaderData<typeof loader>();

return (
<>
<h1 className="py-2">Login</h1>
{ data ? <div className="alert alert-warning">It looks like you are already logged in! <RemixLink className="alert-link" to="/auth/">View my user info</RemixLink></div> : <></> }
<form action="/auth/github" method="post">
<button type="submit" className="btn btn-primary">Login with Github</button>
</form>
</>
)
}
57 changes: 57 additions & 0 deletions app/routes/auth.logout[.]html.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { Link as RemixLink, useLoaderData } from "@remix-run/react";
import { cookieStorage } from "~/services/session.server";
import { authenticator } from "~/services/auth.server";

type AlertMessage = {
alert: string;
text: string;
};

export async function loader({request}: LoaderFunctionArgs): Promise<AlertMessage | null> {
const session = await cookieStorage.getSession(request.headers.get("Cookie"));

const message = session.get("logout") as AlertMessage;

console.log('logout message', JSON.stringify(message));
//return logout(request); logouts need to be POSTs
return null;
}

export async function action({ request }: ActionFunctionArgs) {

// don't pass request.headers.get("cookie") here
let session = await cookieStorage.getSession(request.headers.get("Cookie"));

const user = session.get("user");
session = await cookieStorage.getSession(); // create empty session for flash msg
let message:AlertMessage;
if (user) {
message = { alert: "success", text: "You have been logged out" };
} else {
message = { alert: "error", text: "You were not logged in"};
}
session.flash("logout", message);

await authenticator.logout(request, {
redirectTo: "/auth/logout.html",
headers: { "Set-Cookie": await cookieStorage.commitSession(session) },
});
}

export default function AuthLogout() {
const data = useLoaderData<typeof loader>();

const message = data || { alert: "info", text: "You are logged out!" };

return (
<>
<h1 className="py-2">Logout</h1>
<p>{message.text}</p>
<p>
<RemixLink className="btn btn-primary mx-2" to="/auth/">Login</RemixLink>
<RemixLink className="btn btn-primary mx-2" to="/">Home</RemixLink>
</p>
</>
);
}
18 changes: 18 additions & 0 deletions app/services/AuthContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createContext } from "react";
import { User } from "~/types/User";


export const AnonymousUser:User = {
email: "",
avatar: "",
displayName: "Anonymous",
provider: "anonymous",
providerName: "anonymous",
id: "anonymous:anonymous",
isAnonymous: true,

}

const AuthContext = createContext<User>(AnonymousUser);

export { AuthContext }
Loading

0 comments on commit 7a52429

Please sign in to comment.