Skip to content

Commit

Permalink
added auth pages
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-burel committed Jan 8, 2021
1 parent d641239 commit c77288f
Show file tree
Hide file tree
Showing 16 changed files with 656 additions and 2 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@next/mdx": "^10.0.2",
"@vulcanjs/demo": "^0.0.7",
"@vulcanjs/mdx": "^0.0.7",
"@vulcanjs/meteor-legacy": "^0.1.8",
"@vulcanjs/mongo": "^0.1.8",
"@vulcanjs/react-hooks": "^0.1.8",
"apollo-server-express": "2.14.2",
Expand All @@ -72,8 +73,10 @@
"lodash": "^4.17.19",
"mongoose": "^5.9.19",
"next": "^10.0.2",
"next-connect": "^0.9.1",
"next-i18next": "^5.1.0",
"next-mdx-enhanced": "^4.0.0",
"passport": "^0.4.1",
"passport-local": "1.0.0",
"polished": "^3.6.5",
"postcss-nested": "^4.2.1",
Expand All @@ -85,7 +88,8 @@
"styled-components": "^5.1.1",
"styled-components-modifiers": "^1.2.5",
"styled-jsx": "^3.3.0",
"styled-jsx-plugin-postcss": "^3.0.2"
"styled-jsx-plugin-postcss": "^3.0.2",
"swr": "^0.4.0"
},
"devDependencies": {
"@babel/core": "^7.10.2",
Expand Down
4 changes: 3 additions & 1 deletion src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
* so that changing layout for certain pages is easier
*/
import { typeScale } from "~/lib/style/typography";
import Footer from "./Footer";

interface AppLayoutProps {
children: React.ReactNode;
}

const AppLayout = ({ children }: AppLayoutProps) => (
<div className="global">
{children}
<main>{children}</main>
<Footer />
<style jsx global>{`
/* FIXME: ignore errors when using "vscode-styled-jsx", as we also use PostCSS */
/* Typescale */
Expand Down
76 changes: 76 additions & 0 deletions src/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Typography } from "@material-ui/core";
/**
* TODO: the useUser hook doesn't seem to be updated on route change when the component is put into _app
*/
// Taken from Next Passport example
import Link from "next/link";
import { useUser } from "~/components/user/hooks";

const Header = () => {
const user = useUser();
return (
<footer>
<nav>
<ul>
<li>
<Link href="/">
<a>Home</a>
</Link>
</li>
{user ? (
<>
<li>
<Link href="/profile">
<a>Profile</a>
</Link>
</li>
<li>
<a href="/api/logout">Logout</a>
</li>
</>
) : (
<li>
<Link href="/login">
<a>Login</a>
</Link>
</li>
)}
</ul>
</nav>
<style jsx>{`
nav {
max-width: 42rem;
margin: 0 auto;
padding: 0.2rem 1.25rem;
max-width: 1000px;
margin: auto;
}
ul {
display: flex;
list-style: none;
margin-left: 0;
padding-left: 0;
}
li {
margin-right: 1rem;
}
li:first-child {
margin-left: auto;
}
a {
color: violet;
text-decoration: none;
}
footer {
color: #000;
border-top: 1px solid;
border-image-source: linear-gradient(10deg, #e1009855, #3f77fa55);
border-image-slice: 1;
border-color: #3f77fa;
}
`}</style>
</footer>
);
};

export default Header;
82 changes: 82 additions & 0 deletions src/components/user/form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Link from "next/link";

const UserForm = ({ isLogin, errorMessage, onSubmit }) => (
<form onSubmit={onSubmit}>
<label>
<span>Email</span>
<input type="text" name="email" required />
</label>
<label>
<span>Password</span>
<input type="password" name="password" required />
</label>
{!isLogin && (
<label>
<span>Repeat password</span>
<input type="password" name="rpassword" required />
</label>
)}

<div className="submit">
{isLogin ? (
<>
<Link href="/signup">
<a>I don't have an account</a>
</Link>
<button type="submit">Login</button>
</>
) : (
<>
<Link href="/login">
<a>I already have an account</a>
</Link>
<button type="submit">Signup</button>
</>
)}
</div>

{errorMessage && <p className="error">{errorMessage}</p>}

<style jsx>{`
form,
label {
display: flex;
flex-flow: column;
}
label > span {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.submit {
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
}
.submit > a {
text-decoration: none;
}
.submit > button {
padding: 0.5rem 1rem;
cursor: pointer;
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
}
.submit > button:hover {
border-color: #888;
}
.error {
color: brown;
margin: 1rem 0 0;
}
`}</style>
</form>
);

export default UserForm;
39 changes: 39 additions & 0 deletions src/components/user/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// It won't reload if there are no remount => we need to find a way to mutate on login
// @see https://github.com/vercel/next.js/discussions/19601
import { useEffect } from "react";
import Router from "next/router";
import useSWR from "swr";

const fetcher = (url) =>
fetch(url)
.then((r) => r.json())
.then((data) => {
return { user: data?.user || null };
});

export function useUser({
redirectTo,
redirectIfFound,
}: {
redirectTo?: string;
redirectIfFound?: boolean;
} = {}) {
const { data, error } = useSWR("/api/user", fetcher);
const user = data?.user;
const finished = Boolean(data);
const hasUser = Boolean(user);

useEffect(() => {
if (!redirectTo || !finished) return;
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !hasUser) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && hasUser)
) {
Router.push(redirectTo);
}
}, [redirectTo, redirectIfFound, finished, hasUser]);

return error ? null : user;
}
38 changes: 38 additions & 0 deletions src/components/user/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Head from "next/head";
import Footer from "~/components/layout/Footer";

const Layout = (props) => (
<>
<Head>
<title>With Cookies</title>
</Head>

<main>
<div className="container">{props.children}</div>
</main>

{/*<Footer />*/}

<style jsx global>{`
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, Noto Sans, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.container {
max-width: 42rem;
margin: 0 auto;
padding: 2rem 1.25rem;
}
`}</style>
</>
);

export default Layout;
39 changes: 39 additions & 0 deletions src/pages/api/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import passport from "passport";
import nextConnect from "next-connect";
import { localStrategy } from "~/api/passport/password-local";
import { encryptSession } from "~/api/passport/iron";
import { setTokenCookie } from "~/api/passport/auth-cookies";
import { NextApiRequest, NextApiResponse } from "next";

const authenticate = (method, req, res): Promise<any> =>
new Promise((resolve, reject) => {
passport.authenticate(method, { session: false }, (error, token) => {
if (error) {
reject(error);
} else {
resolve(token);
}
})(req, res);
});

passport.use(localStrategy);

// NOTE: adding NextApiRequest, NextApiResponse is required to get the right typings in next-connect
// this is the normal behaviour
export default nextConnect<NextApiRequest, NextApiResponse>()
.use(passport.initialize())
.post(async (req, res) => {
try {
const user = await authenticate("local", req, res);
// session is the payload to save in the token, it may contain basic info about the user
const session = { ...user };
// The token is a string with the encrypted session
const token = await encryptSession(session);

setTokenCookie(res, token);
res.status(200).send({ done: true });
} catch (error) {
console.error(error);
res.status(401).send(error.message);
}
});
11 changes: 11 additions & 0 deletions src/pages/api/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextApiRequest, NextApiResponse } from "next";
import { removeTokenCookie } from "~/api/passport/auth-cookies";

export default async function logout(
req: NextApiRequest,
res: NextApiResponse
) {
removeTokenCookie(res);
res.writeHead(302, { Location: "/" });
res.end();
}
24 changes: 24 additions & 0 deletions src/pages/api/signup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Request } from "express";
import { createMutator } from "@vulcanjs/graphql";
import { NextApiRequest, NextApiResponse } from "next";
import { User } from "~/models/user";

// TODO: factor the context creation so we can reuse it for graphql and REST endpoints
import { contextFromReq } from "~/api/context";
export default async function signup(
req: NextApiRequest,
res: NextApiResponse
) {
try {
// NOTE: the mutator is the function used by the create mutations in Vulcan
// we need to use it to ensure that we run all callbacks associated to the user collection
const user = req.body;
// TODO: check if this is ok to compute the context from a NextApiRequest like this
const context = await contextFromReq(req as unknown as Request)
await createMutator({ model: User, data: user, context });
res.status(200).send({ done: true });
} catch (error) {
console.error(error);
res.status(500).end(error.message);
}
}
11 changes: 11 additions & 0 deletions src/pages/api/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getSession } from "~/api/passport/iron";
import { UserConnector } from "~/models/user";

export default async function user(req, res) {
const session = await getSession(req);
// Get fresh data about the user
const user = session?._id
? await UserConnector.findOneById(session._id)
: null;
res.status(200).json({ user: user || null });
}
Loading

0 comments on commit c77288f

Please sign in to comment.