Skip to content

Commit 7a52429

Browse files
committed
Add login with Github
1 parent b34ed50 commit 7a52429

17 files changed

+401
-26
lines changed

app/components/AuthButton.tsx

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Link as RemixLink, useRouteLoaderData } from "@remix-run/react";
2+
import { PiUserCircle, PiUserCircleFill } from "react-icons/pi";
3+
import { User } from "~/types/User";
4+
5+
export function AuthButton() {
6+
const user = useRouteLoaderData<User|null>("root");
7+
8+
let authImage:JSX.Element = <PiUserCircle title={"Not logged in"} />;
9+
if (user) {
10+
if (user.avatar) {
11+
authImage = <img src={user.avatar} alt={user.displayName} className="rounded-circle" style={{"width": "1.75rem", "height": "1.75rem"}} />
12+
} else {
13+
authImage = <PiUserCircleFill title={user.displayName} />;
14+
}
15+
}
16+
17+
console.log('authbutton', JSON.stringify(user));
18+
return (
19+
<>
20+
<RemixLink className="d-none d-sm-inline text-dark" to="/auth/" style={{ "fontSize": "1.75rem" }}>
21+
{ authImage }
22+
</RemixLink>
23+
</>
24+
)
25+
}
26+

app/components/Footer.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { ColorSchemeToggle } from "~/components/ColorSchemeToggle";
22

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

1010
export function Footer() {
@@ -15,7 +15,7 @@ export function Footer() {
1515
{link.label}
1616
</a>);
1717
if (index < links.length - 1) {
18-
initial.push(<span className="mx-1" key="key{{index}}">|</span>);
18+
initial.push(<span className="mx-1" key={`key${index}`}>|</span>);
1919
}
2020
}
2121
);
@@ -27,7 +27,7 @@ export function Footer() {
2727
{ initial }
2828
</small>
2929

30-
<ColorSchemeToggle />
30+
<ColorSchemeToggle key="cst"/>
3131
</footer>
3232
</>
3333
)

app/components/Navbar.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { PiBlueprint, PiBlueprintBold, PiLink, PiMagnifyingGlass, PiMagnifyingGl
22
import { Link as RemixLink } from "@remix-run/react";
33

44
import RegexZoneSvg from './RegexZoneSvg';
5-
import { NavbarLink, NavbarLinkItem } from '~/components/NavbarLink';
5+
import { NavbarLink, NavbarLinkItem } from './NavbarLink';
6+
import { AuthButton } from './AuthButton';
67

78
const links:NavbarLinkItem[] = [
89
{ link: '/patterns/', label: 'Patterns', icon: <PiBlueprint />, icon_bold: <PiBlueprintBold /> },
@@ -19,14 +20,15 @@ export function Navbar() {
1920
return (
2021
<>
2122
<nav className="navbar navbar-expand bg-body-tertiary border-bottom">
22-
<div className="container-lg">
23-
<RemixLink className="navbar-brand fs-4 fw-bold" to="/">
23+
<div className="container-lg d-flex">
24+
<RemixLink className="navbar-brand fs-4 fw-bold flex-grow-1" to="/">
2425
<RegexZoneSvg height={'2rem'} className="pe-2 d-none d-md-inline" />
2526
Regex Zone
2627
</RemixLink>
27-
<ul className="navbar-nav">
28+
<ul className="navbar-nav mt-1">
2829
{items}
2930
</ul>
31+
<AuthButton />
3032
</div>
3133
</nav>
3234
</>

app/root.tsx

+25-16
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,26 @@ import {
99
} from "@remix-run/react";
1010
import { Navbar } from "~/components/Navbar";
1111
import { Footer } from "~/components/Footer";
12+
import { LoaderFunctionArgs } from "@remix-run/node";
13+
14+
import { authenticator } from "~/services/auth.server";
15+
16+
export async function loader({ request }: LoaderFunctionArgs) {
17+
return await authenticator.isAuthenticated(request);
18+
}
1219

1320
export function Layout({ children }: { children: React.ReactNode }) {
1421
return (
1522
<html lang="en">
1623
<head>
1724
<meta charSet="utf-8" />
1825
<meta name="viewport" content="width=device-width, initial-scale=1" />
19-
<link
20-
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
21-
rel="stylesheet"
22-
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
23-
crossOrigin="anonymous"
24-
/>
26+
<link
27+
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
28+
rel="stylesheet"
29+
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
30+
crossOrigin="anonymous"
31+
/>
2532
<Meta />
2633
<Links />
2734
<style>{`
@@ -35,7 +42,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
3542
<body>
3643
<Navbar />
3744
<div className="container-lg">
38-
{children}
45+
{children}
3946
</div>
4047
<Footer />
4148
<ScrollRestoration />
@@ -46,21 +53,23 @@ export function Layout({ children }: { children: React.ReactNode }) {
4653
}
4754

4855
export default function App() {
49-
return <Outlet />;
56+
return (
57+
<Outlet />
58+
);
5059
}
5160

5261
export function ErrorBoundary() {
5362
const error = useRouteError();
5463
return (
5564
<>
56-
<h1 className="py-2">Error</h1>
57-
<div>
58-
{isRouteErrorResponse(error)
59-
? `${error.status} ${error.statusText}`
60-
: error instanceof Error
61-
? error.message
62-
: "Unknown Error"}
63-
</div>
65+
<h1 className="py-2">Error</h1>
66+
<div>
67+
{isRouteErrorResponse(error)
68+
? `${error.status} ${error.statusText}`
69+
: error instanceof Error
70+
? error.message
71+
: "Unknown Error"}
72+
</div>
6473
</>
6574
);
6675
}

app/routes/auth._index.tsx

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { LoaderFunctionArgs } from "@remix-run/node";
2+
import { Link as RemixLink, useLoaderData } from "@remix-run/react";
3+
import { authenticator } from "~/services/auth.server";
4+
import { cookieStorage } from "~/services/session.server";
5+
6+
export async function loader({ request }: LoaderFunctionArgs) {
7+
//console.log('in loader', (await cookieStorage.getSession(request.headers.get("Cookie"))).get("user"))
8+
const session = await cookieStorage.getSession(request.headers.get("Cookie"));
9+
const sessionUser = session.get("user");
10+
const authUser = await authenticator.isAuthenticated(request);
11+
return {
12+
//
13+
user: sessionUser,
14+
sessionUser,
15+
authUser,
16+
session: (await cookieStorage.getSession(request.headers.get("Cookie")))
17+
};
18+
}
19+
20+
function LoginSection() {
21+
return (
22+
<>
23+
<p>You are not logged in!</p>
24+
<RemixLink className="btn btn-primary" to="login.html">Login</RemixLink>
25+
</>
26+
)
27+
}
28+
29+
function LogoutSection({ user }: { user: any }) {
30+
return (
31+
<>
32+
<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>
33+
<p>Your email is <span className="border rounded bg-body-tertiary text-body-secondary p-2">{user.email}</span></p>
34+
<p>Your profile image is <img className="px-2" src={user.avatar} alt={user.displayName} style={{"height":"2em"}} /></p>
35+
<form action="/auth/logout.html" method="post">
36+
<input type="submit" className="btn btn-primary" value="Logout" />
37+
</form>
38+
</>
39+
)
40+
}
41+
42+
export default function AuthIndex() {
43+
const data = useLoaderData<typeof loader>();
44+
45+
return (
46+
<>
47+
<h1 className="py-2">Authentication</h1>
48+
{ data.user ? <LogoutSection user={data.user} /> : <LoginSection/> }
49+
<details className="pt-3">
50+
<summary>Raw Auth User Data</summary>
51+
<pre>{JSON.stringify(data.user, null, 4)}</pre>
52+
</details>
53+
<details className="">
54+
<summary>Raw Session User Data</summary>
55+
<pre>{JSON.stringify(data.user, null, 4)}</pre>
56+
</details>
57+
<details>
58+
<summary>Raw Session Data</summary>
59+
<pre>{JSON.stringify(data.session, null, 4)}</pre>
60+
</details>
61+
</>
62+
)
63+
}

app/routes/auth.github.callback.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { redirect, type LoaderFunctionArgs } from "@remix-run/node";
2+
import { authenticator } from "~/services/auth.server";
3+
import { cookieStorage } from "~/services/session.server";
4+
5+
export async function loader({ request }: LoaderFunctionArgs) {
6+
const user = await authenticator.authenticate("github", request);
7+
8+
console.log('user in callback', JSON.stringify(user));
9+
if (user != null) {
10+
const session = await cookieStorage.getSession(request.headers.get("Cookie"));
11+
session.set("user", user);
12+
return redirect('/auth/', { headers: { "Set-Cookie": await cookieStorage.commitSession(session) }});
13+
}
14+
return redirect("/auth/");
15+
16+
/*
17+
return authenticator.authenticate("github", request, {
18+
successRedirect: "/auth/sucess.html",
19+
failureRedirect: "/auth/failure.html",
20+
});
21+
*/
22+
}

app/routes/auth.github.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { ActionFunctionArgs } from "@remix-run/node";
2+
import { redirect } from "@remix-run/node";
3+
import { authenticator } from "~/services/auth.server";
4+
5+
export async function loader() {
6+
return redirect("/auth/");
7+
}
8+
9+
export async function action({ request }: ActionFunctionArgs) {
10+
return authenticator.authenticate("github", request, {
11+
successRedirect: "/auth/",
12+
failureRedirect: "/auth/",
13+
});
14+
}

app/routes/auth.login[.]html.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { LoaderFunctionArgs } from "@remix-run/node";
2+
import { Link as RemixLink, useLoaderData } from "@remix-run/react";
3+
import { PiGithubLogoBold } from "react-icons/pi";
4+
import { authenticator } from "~/services/auth.server";
5+
6+
export async function loader({ request }: LoaderFunctionArgs) {
7+
return await authenticator.isAuthenticated(request);
8+
}
9+
10+
export default function AuthIndex() {
11+
const data = useLoaderData<typeof loader>();
12+
13+
return (
14+
<>
15+
<h1 className="py-2">Login</h1>
16+
{ 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> : <></> }
17+
<form action="/auth/github" method="post">
18+
<button type="submit" className="btn btn-primary">Login with Github</button>
19+
</form>
20+
</>
21+
)
22+
}

app/routes/auth.logout[.]html.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
2+
import { Link as RemixLink, useLoaderData } from "@remix-run/react";
3+
import { cookieStorage } from "~/services/session.server";
4+
import { authenticator } from "~/services/auth.server";
5+
6+
type AlertMessage = {
7+
alert: string;
8+
text: string;
9+
};
10+
11+
export async function loader({request}: LoaderFunctionArgs): Promise<AlertMessage | null> {
12+
const session = await cookieStorage.getSession(request.headers.get("Cookie"));
13+
14+
const message = session.get("logout") as AlertMessage;
15+
16+
console.log('logout message', JSON.stringify(message));
17+
//return logout(request); logouts need to be POSTs
18+
return null;
19+
}
20+
21+
export async function action({ request }: ActionFunctionArgs) {
22+
23+
// don't pass request.headers.get("cookie") here
24+
let session = await cookieStorage.getSession(request.headers.get("Cookie"));
25+
26+
const user = session.get("user");
27+
session = await cookieStorage.getSession(); // create empty session for flash msg
28+
let message:AlertMessage;
29+
if (user) {
30+
message = { alert: "success", text: "You have been logged out" };
31+
} else {
32+
message = { alert: "error", text: "You were not logged in"};
33+
}
34+
session.flash("logout", message);
35+
36+
await authenticator.logout(request, {
37+
redirectTo: "/auth/logout.html",
38+
headers: { "Set-Cookie": await cookieStorage.commitSession(session) },
39+
});
40+
}
41+
42+
export default function AuthLogout() {
43+
const data = useLoaderData<typeof loader>();
44+
45+
const message = data || { alert: "info", text: "You are logged out!" };
46+
47+
return (
48+
<>
49+
<h1 className="py-2">Logout</h1>
50+
<p>{message.text}</p>
51+
<p>
52+
<RemixLink className="btn btn-primary mx-2" to="/auth/">Login</RemixLink>
53+
<RemixLink className="btn btn-primary mx-2" to="/">Home</RemixLink>
54+
</p>
55+
</>
56+
);
57+
}

app/services/AuthContext.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { createContext } from "react";
2+
import { User } from "~/types/User";
3+
4+
5+
export const AnonymousUser:User = {
6+
email: "",
7+
avatar: "",
8+
displayName: "Anonymous",
9+
provider: "anonymous",
10+
providerName: "anonymous",
11+
id: "anonymous:anonymous",
12+
isAnonymous: true,
13+
14+
}
15+
16+
const AuthContext = createContext<User>(AnonymousUser);
17+
18+
export { AuthContext }

0 commit comments

Comments
 (0)