chore(examples): use default prettier for examples/templates (#60530)
## Description This PR ensures that the default prettier config is used for examples and templates. This config is compatible with `prettier@3` as well (upgrading prettier is bigger change that can be a future PR). ## Changes - Updated `.prettierrc.json` in root with `"trailingComma": "es5"` (will be needed upgrading to prettier@3) - Added `examples/.prettierrc.json` with default config (this will change every example) - Added `packages/create-next-app/templates/.prettierrc.json` with default config (this will change every template) ## Related - Fixes #54402 - Closes #54409
This commit is contained in:
parent
98b99e408b
commit
4466ba436b
2559 changed files with 21384 additions and 21309 deletions
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"semi": false
|
"semi": false
|
||||||
}
|
}
|
||||||
|
|
5
examples/.prettierrc.json
Normal file
5
examples/.prettierrc.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": false,
|
||||||
|
"semi": true
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
import Link, { LinkProps } from 'next/link'
|
import Link, { LinkProps } from "next/link";
|
||||||
import React, { PropsWithChildren, useState, useEffect } from 'react'
|
import React, { PropsWithChildren, useState, useEffect } from "react";
|
||||||
|
|
||||||
type ActiveLinkProps = LinkProps & {
|
type ActiveLinkProps = LinkProps & {
|
||||||
className?: string
|
className?: string;
|
||||||
activeClassName: string
|
activeClassName: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ActiveLink = ({
|
const ActiveLink = ({
|
||||||
children,
|
children,
|
||||||
|
@ -13,8 +13,8 @@ const ActiveLink = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: PropsWithChildren<ActiveLinkProps>) => {
|
}: PropsWithChildren<ActiveLinkProps>) => {
|
||||||
const { asPath, isReady } = useRouter()
|
const { asPath, isReady } = useRouter();
|
||||||
const [computedClassName, setComputedClassName] = useState(className)
|
const [computedClassName, setComputedClassName] = useState(className);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if the router fields are updated client-side
|
// Check if the router fields are updated client-side
|
||||||
|
@ -23,19 +23,19 @@ const ActiveLink = ({
|
||||||
// Static route will be matched via props.href
|
// Static route will be matched via props.href
|
||||||
const linkPathname = new URL(
|
const linkPathname = new URL(
|
||||||
(props.as || props.href) as string,
|
(props.as || props.href) as string,
|
||||||
location.href
|
location.href,
|
||||||
).pathname
|
).pathname;
|
||||||
|
|
||||||
// Using URL().pathname to get rid of query and hash
|
// Using URL().pathname to get rid of query and hash
|
||||||
const activePathname = new URL(asPath, location.href).pathname
|
const activePathname = new URL(asPath, location.href).pathname;
|
||||||
|
|
||||||
const newClassName =
|
const newClassName =
|
||||||
linkPathname === activePathname
|
linkPathname === activePathname
|
||||||
? `${className} ${activeClassName}`.trim()
|
? `${className} ${activeClassName}`.trim()
|
||||||
: className
|
: className;
|
||||||
|
|
||||||
if (newClassName !== computedClassName) {
|
if (newClassName !== computedClassName) {
|
||||||
setComputedClassName(newClassName)
|
setComputedClassName(newClassName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
@ -46,13 +46,13 @@ const ActiveLink = ({
|
||||||
activeClassName,
|
activeClassName,
|
||||||
className,
|
className,
|
||||||
computedClassName,
|
computedClassName,
|
||||||
])
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className={computedClassName} {...props}>
|
<Link className={computedClassName} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ActiveLink
|
export default ActiveLink;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import ActiveLink from './ActiveLink'
|
import ActiveLink from "./ActiveLink";
|
||||||
|
|
||||||
const Nav = () => (
|
const Nav = () => (
|
||||||
<nav>
|
<nav>
|
||||||
|
@ -8,7 +8,7 @@ const Nav = () => (
|
||||||
}
|
}
|
||||||
|
|
||||||
.active:after {
|
.active:after {
|
||||||
content: ' (current page)';
|
content: " (current page)";
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<ul className="nav">
|
<ul className="nav">
|
||||||
|
@ -39,6 +39,6 @@ const Nav = () => (
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default Nav
|
export default Nav;
|
||||||
|
|
|
@ -3,9 +3,9 @@ module.exports = {
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: '/blog',
|
source: "/blog",
|
||||||
destination: '/news',
|
destination: "/news",
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
import Nav from '../components/Nav'
|
import Nav from "../components/Nav";
|
||||||
|
|
||||||
const SlugPage = () => {
|
const SlugPage = () => {
|
||||||
const { asPath } = useRouter()
|
const { asPath } = useRouter();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Nav />
|
<Nav />
|
||||||
<p>Hello, I'm the {asPath} page</p>
|
<p>Hello, I'm the {asPath} page</p>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SlugPage
|
export default SlugPage;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import Nav from '../components/Nav'
|
import Nav from "../components/Nav";
|
||||||
|
|
||||||
const AboutPage = () => (
|
const AboutPage = () => (
|
||||||
<>
|
<>
|
||||||
<Nav />
|
<Nav />
|
||||||
<p>Hello, I'm the about page</p>
|
<p>Hello, I'm the about page</p>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default AboutPage
|
export default AboutPage;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import Nav from '../components/Nav'
|
import Nav from "../components/Nav";
|
||||||
|
|
||||||
const IndexPage = () => (
|
const IndexPage = () => (
|
||||||
<>
|
<>
|
||||||
<Nav />
|
<Nav />
|
||||||
<p>Hello, I'm the index page</p>
|
<p>Hello, I'm the index page</p>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default IndexPage
|
export default IndexPage;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import Nav from '../components/Nav'
|
import Nav from "../components/Nav";
|
||||||
|
|
||||||
const News = () => (
|
const News = () => (
|
||||||
<>
|
<>
|
||||||
<Nav />
|
<Nav />
|
||||||
<p>Hello, I'm the news page</p>
|
<p>Hello, I'm the news page</p>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default News
|
export default News;
|
||||||
|
|
16
examples/amp/additional.d.ts
vendored
16
examples/amp/additional.d.ts
vendored
|
@ -3,16 +3,16 @@
|
||||||
// Only the intrinsic elements defined here will be accepted, and only with the attributes defined here
|
// Only the intrinsic elements defined here will be accepted, and only with the attributes defined here
|
||||||
declare namespace JSX {
|
declare namespace JSX {
|
||||||
interface AmpImg {
|
interface AmpImg {
|
||||||
alt?: string
|
alt?: string;
|
||||||
src?: string
|
src?: string;
|
||||||
width?: string | number
|
width?: string | number;
|
||||||
height?: string | number
|
height?: string | number;
|
||||||
layout?: string
|
layout?: string;
|
||||||
fallback?: string
|
fallback?: string;
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
interface IntrinsicElements {
|
interface IntrinsicElements {
|
||||||
'amp-img': AmpImg
|
"amp-img": AmpImg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
type BylineProps = {
|
type BylineProps = {
|
||||||
author: string
|
author: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Byline({ author }: BylineProps) {
|
export default function Byline({ author }: BylineProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -13,5 +13,5 @@ export default function Byline({ author }: BylineProps) {
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children }: LayoutProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -14,5 +14,5 @@ export default function Layout({ children }: LayoutProps) {
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import Head from 'next/head'
|
import Head from "next/head";
|
||||||
import { useAmp } from 'next/amp'
|
import { useAmp } from "next/amp";
|
||||||
import Byline from '../components/Byline'
|
import Byline from "../components/Byline";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
amp: 'hybrid',
|
amp: "hybrid",
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function DogPage() {
|
export default function DogPage() {
|
||||||
const isAmp = useAmp()
|
const isAmp = useAmp();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -17,14 +17,14 @@ export default function DogPage() {
|
||||||
<h1>The Dog (Hybrid AMP Page)</h1>
|
<h1>The Dog (Hybrid AMP Page)</h1>
|
||||||
<Byline author="Meow Meow Fuzzyface" />
|
<Byline author="Meow Meow Fuzzyface" />
|
||||||
<p>
|
<p>
|
||||||
<a href={isAmp ? '/dog' : '/dog?amp=1'}>
|
<a href={isAmp ? "/dog" : "/dog?amp=1"}>
|
||||||
{isAmp ? 'View Non-AMP' : 'View AMP'} Version
|
{isAmp ? "View Non-AMP" : "View AMP"} Version
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="caption">Woooooooooooof</p>
|
<p className="caption">Woooooooooooof</p>
|
||||||
<p>
|
<p>
|
||||||
Wafer donut candy soufflé{' '}
|
Wafer donut candy soufflé{" "}
|
||||||
<a href={isAmp ? '/?amp=1' : '/'}>lemon drops</a> icing. Marzipan gummi
|
<a href={isAmp ? "/?amp=1" : "/"}>lemon drops</a> icing. Marzipan gummi
|
||||||
bears pie danish lollipop pudding powder gummi bears sweet. Pie sweet
|
bears pie danish lollipop pudding powder gummi bears sweet. Pie sweet
|
||||||
roll sweet roll topping chocolate bar dragée pudding chocolate cake.
|
roll sweet roll topping chocolate bar dragée pudding chocolate cake.
|
||||||
Croissant sweet chocolate bar cheesecake candy canes. Tootsie roll icing
|
Croissant sweet chocolate bar cheesecake candy canes. Tootsie roll icing
|
||||||
|
@ -90,5 +90,5 @@ export default function DogPage() {
|
||||||
tiramisu.
|
tiramisu.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import Head from 'next/head'
|
import Head from "next/head";
|
||||||
import { useAmp } from 'next/amp'
|
import { useAmp } from "next/amp";
|
||||||
import Layout from '../components/Layout'
|
import Layout from "../components/Layout";
|
||||||
import Byline from '../components/Byline'
|
import Byline from "../components/Byline";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
amp: true,
|
amp: true,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function IndexPage() {
|
export default function IndexPage() {
|
||||||
const isAmp = useAmp()
|
const isAmp = useAmp();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
@ -35,7 +35,7 @@ export default function IndexPage() {
|
||||||
></amp-img>
|
></amp-img>
|
||||||
</amp-img>
|
</amp-img>
|
||||||
<p>
|
<p>
|
||||||
Cat ipsum dolor <a href={isAmp ? '/dog?amp=1' : '/dog'}>sit amet</a>,
|
Cat ipsum dolor <a href={isAmp ? "/dog?amp=1" : "/dog"}>sit amet</a>,
|
||||||
eat grass, throw it back up but refuse to leave cardboard box or groom
|
eat grass, throw it back up but refuse to leave cardboard box or groom
|
||||||
yourself 4 hours - checked, have your beauty sleep 18 hours - checked,
|
yourself 4 hours - checked, have your beauty sleep 18 hours - checked,
|
||||||
be fabulous for the rest of the day - checked!. Hide from vacuum
|
be fabulous for the rest of the day - checked!. Hide from vacuum
|
||||||
|
@ -233,5 +233,5 @@ export default function IndexPage() {
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export default function NormalPage() {
|
export default function NormalPage() {
|
||||||
return <p>I'm just a normal old page, no AMP for me</p>
|
return <p>I'm just a normal old page, no AMP for me</p>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||||
enabled: process.env.ANALYZE === 'true',
|
enabled: process.env.ANALYZE === "true",
|
||||||
})
|
});
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
// any configs you need
|
// any configs you need
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = withBundleAnalyzer(nextConfig)
|
module.exports = withBundleAnalyzer(nextConfig);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const About = () => {
|
const About = () => {
|
||||||
return <div>About us</div>
|
return <div>About us</div>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default About
|
export default About;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const Contact = () => {
|
const Contact = () => {
|
||||||
return <div>This is the contact page.</div>
|
return <div>This is the contact page.</div>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Contact
|
export default Contact;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { NextPage, GetStaticProps } from 'next'
|
import { NextPage, GetStaticProps } from "next";
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import { faker } from '@faker-js/faker'
|
import { faker } from "@faker-js/faker";
|
||||||
|
|
||||||
type IndexProps = {
|
type IndexProps = {
|
||||||
name: string
|
name: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const Index: NextPage<IndexProps> = ({ name }) => {
|
const Index: NextPage<IndexProps> = ({ name }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -15,16 +15,16 @@ const Index: NextPage<IndexProps> = ({ name }) => {
|
||||||
<Link href="/about">About Page</Link>
|
<Link href="/about">About Page</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Index
|
export default Index;
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async () => {
|
export const getStaticProps: GetStaticProps = async () => {
|
||||||
// The name will be generated at build time only
|
// The name will be generated at build time only
|
||||||
const name = faker.name.findName()
|
const name = faker.name.findName();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { name },
|
props: { name },
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,54 +1,54 @@
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from "react";
|
||||||
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
|
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
|
||||||
import { SchemaLink } from '@apollo/client/link/schema'
|
import { SchemaLink } from "@apollo/client/link/schema";
|
||||||
import { schema } from '../apollo/schema'
|
import { schema } from "../apollo/schema";
|
||||||
import merge from 'deepmerge'
|
import merge from "deepmerge";
|
||||||
|
|
||||||
let apolloClient
|
let apolloClient;
|
||||||
|
|
||||||
function createIsomorphLink() {
|
function createIsomorphLink() {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === "undefined") {
|
||||||
return new SchemaLink({ schema })
|
return new SchemaLink({ schema });
|
||||||
} else {
|
} else {
|
||||||
return new HttpLink({
|
return new HttpLink({
|
||||||
uri: '/api/graphql',
|
uri: "/api/graphql",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createApolloClient() {
|
function createApolloClient() {
|
||||||
return new ApolloClient({
|
return new ApolloClient({
|
||||||
ssrMode: typeof window === 'undefined',
|
ssrMode: typeof window === "undefined",
|
||||||
link: createIsomorphLink(),
|
link: createIsomorphLink(),
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializeApollo(initialState = null) {
|
export function initializeApollo(initialState = null) {
|
||||||
const _apolloClient = apolloClient ?? createApolloClient()
|
const _apolloClient = apolloClient ?? createApolloClient();
|
||||||
|
|
||||||
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
|
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
|
||||||
// get hydrated here
|
// get hydrated here
|
||||||
if (initialState) {
|
if (initialState) {
|
||||||
// Get existing cache, loaded during client side data fetching
|
// Get existing cache, loaded during client side data fetching
|
||||||
const existingCache = _apolloClient.extract()
|
const existingCache = _apolloClient.extract();
|
||||||
|
|
||||||
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
|
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
|
||||||
const data = merge(initialState, existingCache)
|
const data = merge(initialState, existingCache);
|
||||||
|
|
||||||
// Restore the cache with the merged data
|
// Restore the cache with the merged data
|
||||||
_apolloClient.cache.restore(data)
|
_apolloClient.cache.restore(data);
|
||||||
}
|
}
|
||||||
// For SSG and SSR always create a new Apollo Client
|
// For SSG and SSR always create a new Apollo Client
|
||||||
if (typeof window === 'undefined') return _apolloClient
|
if (typeof window === "undefined") return _apolloClient;
|
||||||
// Create the Apollo Client once in the client
|
// Create the Apollo Client once in the client
|
||||||
if (!apolloClient) apolloClient = _apolloClient
|
if (!apolloClient) apolloClient = _apolloClient;
|
||||||
|
|
||||||
return _apolloClient
|
return _apolloClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useApollo(initialState) {
|
export function useApollo(initialState) {
|
||||||
const store = useMemo(() => initializeApollo(initialState), [initialState])
|
const store = useMemo(() => initializeApollo(initialState), [initialState]);
|
||||||
return store
|
return store;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +1,53 @@
|
||||||
import { createUser, findUser, validatePassword } from '../lib/user'
|
import { createUser, findUser, validatePassword } from "../lib/user";
|
||||||
import { setLoginSession, getLoginSession } from '../lib/auth'
|
import { setLoginSession, getLoginSession } from "../lib/auth";
|
||||||
import { removeTokenCookie } from '../lib/auth-cookies'
|
import { removeTokenCookie } from "../lib/auth-cookies";
|
||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from "graphql";
|
||||||
|
|
||||||
export const resolvers = {
|
export const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
async viewer(_root, _args, context, _info) {
|
async viewer(_root, _args, context, _info) {
|
||||||
try {
|
try {
|
||||||
const session = await getLoginSession(context.req)
|
const session = await getLoginSession(context.req);
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
return findUser({ email: session.email })
|
return findUser({ email: session.email });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new GraphQLError(
|
throw new GraphQLError(
|
||||||
'Authentication token is invalid, please log in',
|
"Authentication token is invalid, please log in",
|
||||||
{
|
{
|
||||||
extensions: {
|
extensions: {
|
||||||
code: 'UNAUTHENTICATED',
|
code: "UNAUTHENTICATED",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
async signUp(_parent, args, _context, _info) {
|
async signUp(_parent, args, _context, _info) {
|
||||||
const user = await createUser(args.input)
|
const user = await createUser(args.input);
|
||||||
return { user }
|
return { user };
|
||||||
},
|
},
|
||||||
async signIn(_parent, args, context, _info) {
|
async signIn(_parent, args, context, _info) {
|
||||||
const user = await findUser({ email: args.input.email })
|
const user = await findUser({ email: args.input.email });
|
||||||
|
|
||||||
if (user && (await validatePassword(user, args.input.password))) {
|
if (user && (await validatePassword(user, args.input.password))) {
|
||||||
const session = {
|
const session = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
};
|
||||||
|
|
||||||
|
await setLoginSession(context.res, session);
|
||||||
|
|
||||||
|
return { user };
|
||||||
}
|
}
|
||||||
|
|
||||||
await setLoginSession(context.res, session)
|
throw new GraphQLError("Invalid email and password combination");
|
||||||
|
|
||||||
return { user }
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new GraphQLError('Invalid email and password combination')
|
|
||||||
},
|
},
|
||||||
async signOut(_parent, _args, context, _info) {
|
async signOut(_parent, _args, context, _info) {
|
||||||
removeTokenCookie(context.res)
|
removeTokenCookie(context.res);
|
||||||
return true
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { makeExecutableSchema } from '@graphql-tools/schema'
|
import { makeExecutableSchema } from "@graphql-tools/schema";
|
||||||
import { typeDefs } from './type-defs'
|
import { typeDefs } from "./type-defs";
|
||||||
import { resolvers } from './resolvers'
|
import { resolvers } from "./resolvers";
|
||||||
|
|
||||||
export const schema = makeExecutableSchema({
|
export const schema = makeExecutableSchema({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { gql } from '@apollo/client'
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
export const typeDefs = gql`
|
export const typeDefs = gql`
|
||||||
type User {
|
type User {
|
||||||
|
@ -36,4 +36,4 @@ export const typeDefs = gql`
|
||||||
signIn(input: SignInInput!): SignInPayload!
|
signIn(input: SignInInput!): SignInPayload!
|
||||||
signOut: Boolean!
|
signOut: Boolean!
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
export default function Field({ name, label, type, autoComplete, required }) {
|
export default function Field({ name, label, type, autoComplete, required }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label id={[name, 'label'].join('-')} htmlFor={[name, 'input'].join('-')}>
|
<label id={[name, "label"].join("-")} htmlFor={[name, "input"].join("-")}>
|
||||||
{label} {required ? <span title="Required">*</span> : undefined}
|
{label} {required ? <span title="Required">*</span> : undefined}
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<input
|
<input
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
id={[name, 'input'].join('-')}
|
id={[name, "input"].join("-")}
|
||||||
name={name}
|
name={name}
|
||||||
required={required}
|
required={required}
|
||||||
type={type}
|
type={type}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,41 @@
|
||||||
import { serialize, parse } from 'cookie'
|
import { serialize, parse } from "cookie";
|
||||||
|
|
||||||
const TOKEN_NAME = 'token'
|
const TOKEN_NAME = "token";
|
||||||
|
|
||||||
export const MAX_AGE = 60 * 60 * 8 // 8 hours
|
export const MAX_AGE = 60 * 60 * 8; // 8 hours
|
||||||
|
|
||||||
export function setTokenCookie(res, token) {
|
export function setTokenCookie(res, token) {
|
||||||
const cookie = serialize(TOKEN_NAME, token, {
|
const cookie = serialize(TOKEN_NAME, token, {
|
||||||
maxAge: MAX_AGE,
|
maxAge: MAX_AGE,
|
||||||
expires: new Date(Date.now() + MAX_AGE * 1000),
|
expires: new Date(Date.now() + MAX_AGE * 1000),
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === "production",
|
||||||
path: '/',
|
path: "/",
|
||||||
sameSite: 'lax',
|
sameSite: "lax",
|
||||||
})
|
});
|
||||||
|
|
||||||
res.setHeader('Set-Cookie', cookie)
|
res.setHeader("Set-Cookie", cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeTokenCookie(res) {
|
export function removeTokenCookie(res) {
|
||||||
const cookie = serialize(TOKEN_NAME, '', {
|
const cookie = serialize(TOKEN_NAME, "", {
|
||||||
maxAge: -1,
|
maxAge: -1,
|
||||||
path: '/',
|
path: "/",
|
||||||
})
|
});
|
||||||
|
|
||||||
res.setHeader('Set-Cookie', cookie)
|
res.setHeader("Set-Cookie", cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCookies(req) {
|
export function parseCookies(req) {
|
||||||
// For API Routes we don't need to parse the cookies.
|
// For API Routes we don't need to parse the cookies.
|
||||||
if (req.cookies) return req.cookies
|
if (req.cookies) return req.cookies;
|
||||||
|
|
||||||
// For pages we do need to parse the cookies.
|
// For pages we do need to parse the cookies.
|
||||||
const cookie = req.headers?.cookie
|
const cookie = req.headers?.cookie;
|
||||||
return parse(cookie || '')
|
return parse(cookie || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTokenCookie(req) {
|
export function getTokenCookie(req) {
|
||||||
const cookies = parseCookies(req)
|
const cookies = parseCookies(req);
|
||||||
return cookies[TOKEN_NAME]
|
return cookies[TOKEN_NAME];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
import Iron from '@hapi/iron'
|
import Iron from "@hapi/iron";
|
||||||
import { MAX_AGE, setTokenCookie, getTokenCookie } from './auth-cookies'
|
import { MAX_AGE, setTokenCookie, getTokenCookie } from "./auth-cookies";
|
||||||
|
|
||||||
const TOKEN_SECRET = process.env.TOKEN_SECRET
|
const TOKEN_SECRET = process.env.TOKEN_SECRET;
|
||||||
|
|
||||||
export async function setLoginSession(res, session) {
|
export async function setLoginSession(res, session) {
|
||||||
const createdAt = Date.now()
|
const createdAt = Date.now();
|
||||||
// Create a session object with a max age that we can validate later
|
// Create a session object with a max age that we can validate later
|
||||||
const obj = { ...session, createdAt, maxAge: MAX_AGE }
|
const obj = { ...session, createdAt, maxAge: MAX_AGE };
|
||||||
const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults)
|
const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults);
|
||||||
|
|
||||||
setTokenCookie(res, token)
|
setTokenCookie(res, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLoginSession(req) {
|
export async function getLoginSession(req) {
|
||||||
const token = getTokenCookie(req)
|
const token = getTokenCookie(req);
|
||||||
|
|
||||||
if (!token) return
|
if (!token) return;
|
||||||
|
|
||||||
const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults)
|
const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults);
|
||||||
const expiresAt = session.createdAt + session.maxAge * 1000
|
const expiresAt = session.createdAt + session.maxAge * 1000;
|
||||||
|
|
||||||
// Validate the expiration date of the session
|
// Validate the expiration date of the session
|
||||||
if (Date.now() > expiresAt) {
|
if (Date.now() > expiresAt) {
|
||||||
throw new Error('Session expired')
|
throw new Error("Session expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
return session
|
return session;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,11 @@ export function getErrorMessage(error) {
|
||||||
for (const graphQLError of error.graphQLErrors) {
|
for (const graphQLError of error.graphQLErrors) {
|
||||||
if (
|
if (
|
||||||
graphQLError.extensions &&
|
graphQLError.extensions &&
|
||||||
graphQLError.extensions.code === 'BAD_USER_INPUT'
|
graphQLError.extensions.code === "BAD_USER_INPUT"
|
||||||
) {
|
) {
|
||||||
return graphQLError.message
|
return graphQLError.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return error.message
|
return error.message;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,46 @@
|
||||||
import crypto from 'crypto'
|
import crypto from "crypto";
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User methods. The example doesn't contain a DB, but for real applications you must use a
|
* User methods. The example doesn't contain a DB, but for real applications you must use a
|
||||||
* db here, such as MongoDB, Fauna, SQL, etc.
|
* db here, such as MongoDB, Fauna, SQL, etc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const users = []
|
const users = [];
|
||||||
|
|
||||||
export async function createUser({ email, password }) {
|
export async function createUser({ email, password }) {
|
||||||
// Here you should create the user and save the salt and hashed password (some dbs may have
|
// Here you should create the user and save the salt and hashed password (some dbs may have
|
||||||
// authentication methods that will do it for you so you don't have to worry about it):
|
// authentication methods that will do it for you so you don't have to worry about it):
|
||||||
const salt = crypto.randomBytes(16).toString('hex')
|
const salt = crypto.randomBytes(16).toString("hex");
|
||||||
const hash = crypto
|
const hash = crypto
|
||||||
.pbkdf2Sync(password, salt, 1000, 64, 'sha512')
|
.pbkdf2Sync(password, salt, 1000, 64, "sha512")
|
||||||
.toString('hex')
|
.toString("hex");
|
||||||
const user = {
|
const user = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
email,
|
email,
|
||||||
hash,
|
hash,
|
||||||
salt,
|
salt,
|
||||||
}
|
};
|
||||||
|
|
||||||
// This is an in memory store for users, there is no data persistence without a proper DB
|
// This is an in memory store for users, there is no data persistence without a proper DB
|
||||||
users.push(user)
|
users.push(user);
|
||||||
|
|
||||||
return user
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here you should lookup for the user in your DB
|
// Here you should lookup for the user in your DB
|
||||||
export async function findUser({ email }) {
|
export async function findUser({ email }) {
|
||||||
// This is an in memory store for users, there is no data persistence without a proper DB
|
// This is an in memory store for users, there is no data persistence without a proper DB
|
||||||
return users.find((user) => user.email === email)
|
return users.find((user) => user.email === email);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare the password of an already fetched user (using `findUser`) and compare the
|
// Compare the password of an already fetched user (using `findUser`) and compare the
|
||||||
// password for a potential match
|
// password for a potential match
|
||||||
export async function validatePassword(user, inputPassword) {
|
export async function validatePassword(user, inputPassword) {
|
||||||
const inputHash = crypto
|
const inputHash = crypto
|
||||||
.pbkdf2Sync(inputPassword, user.salt, 1000, 64, 'sha512')
|
.pbkdf2Sync(inputPassword, user.salt, 1000, 64, "sha512")
|
||||||
.toString('hex')
|
.toString("hex");
|
||||||
const passwordsMatch = user.hash === inputHash
|
const passwordsMatch = user.hash === inputHash;
|
||||||
return passwordsMatch
|
return passwordsMatch;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { ApolloProvider } from '@apollo/client'
|
import { ApolloProvider } from "@apollo/client";
|
||||||
import { useApollo } from '../apollo/client'
|
import { useApollo } from "../apollo/client";
|
||||||
|
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }) {
|
||||||
const apolloClient = useApollo(pageProps.initialApolloState)
|
const apolloClient = useApollo(pageProps.initialApolloState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={apolloClient}>
|
<ApolloProvider client={apolloClient}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Welcome to the about page. Go to the <Link href="/">Home</Link> page.
|
Welcome to the about page. Go to the <Link href="/">Home</Link> page.
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { ApolloServer } from '@apollo/server'
|
import { ApolloServer } from "@apollo/server";
|
||||||
import { startServerAndCreateNextHandler } from '@as-integrations/next'
|
import { startServerAndCreateNextHandler } from "@as-integrations/next";
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { schema } from '../../apollo/schema'
|
import { schema } from "../../apollo/schema";
|
||||||
|
|
||||||
type ExampleContext = {
|
type ExampleContext = {
|
||||||
req: NextApiRequest
|
req: NextApiRequest;
|
||||||
res: NextApiResponse
|
res: NextApiResponse;
|
||||||
}
|
};
|
||||||
|
|
||||||
const apolloServer = new ApolloServer<ExampleContext>({ schema })
|
const apolloServer = new ApolloServer<ExampleContext>({ schema });
|
||||||
|
|
||||||
export default startServerAndCreateNextHandler(apolloServer, {
|
export default startServerAndCreateNextHandler(apolloServer, {
|
||||||
context: async (req, res) => ({ req, res }),
|
context: async (req, res) => ({ req, res }),
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from "react";
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import { gql, useQuery } from '@apollo/client'
|
import { gql, useQuery } from "@apollo/client";
|
||||||
|
|
||||||
const ViewerQuery = gql`
|
const ViewerQuery = gql`
|
||||||
query ViewerQuery {
|
query ViewerQuery {
|
||||||
|
@ -10,36 +10,36 @@ const ViewerQuery = gql`
|
||||||
email
|
email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const { data, loading, error } = useQuery(ViewerQuery)
|
const { data, loading, error } = useQuery(ViewerQuery);
|
||||||
const viewer = data?.viewer
|
const viewer = data?.viewer;
|
||||||
const shouldRedirect = !(loading || error || viewer)
|
const shouldRedirect = !(loading || error || viewer);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldRedirect) {
|
if (shouldRedirect) {
|
||||||
router.push('/signin')
|
router.push("/signin");
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [shouldRedirect])
|
}, [shouldRedirect]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <p>{error.message}</p>
|
return <p>{error.message}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewer) {
|
if (viewer) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
You're signed in as {viewer.email}. Go to{' '}
|
You're signed in as {viewer.email}. Go to{" "}
|
||||||
<Link href="/about">about</Link> page or{' '}
|
<Link href="/about">about</Link> page or{" "}
|
||||||
<Link href="/signout">signout</Link>.
|
<Link href="/signout">signout</Link>.
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <p>Loading...</p>
|
return <p>Loading...</p>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Index
|
export default Index;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useState } from 'react'
|
import { useState } from "react";
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import { gql } from '@apollo/client'
|
import { gql } from "@apollo/client";
|
||||||
import { useMutation, useApolloClient } from '@apollo/client'
|
import { useMutation, useApolloClient } from "@apollo/client";
|
||||||
import { getErrorMessage } from '../lib/form'
|
import { getErrorMessage } from "../lib/form";
|
||||||
import Field from '../components/field'
|
import Field from "../components/field";
|
||||||
|
|
||||||
const SignInMutation = gql`
|
const SignInMutation = gql`
|
||||||
mutation SignInMutation($email: String!, $password: String!) {
|
mutation SignInMutation($email: String!, $password: String!) {
|
||||||
|
@ -15,33 +15,33 @@ const SignInMutation = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
||||||
function SignIn() {
|
function SignIn() {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient();
|
||||||
const [signIn] = useMutation(SignInMutation)
|
const [signIn] = useMutation(SignInMutation);
|
||||||
const [errorMsg, setErrorMsg] = useState()
|
const [errorMsg, setErrorMsg] = useState();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
async function handleSubmit(event) {
|
async function handleSubmit(event) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
|
|
||||||
const emailElement = event.currentTarget.elements.email
|
const emailElement = event.currentTarget.elements.email;
|
||||||
const passwordElement = event.currentTarget.elements.password
|
const passwordElement = event.currentTarget.elements.password;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.resetStore()
|
await client.resetStore();
|
||||||
const { data } = await signIn({
|
const { data } = await signIn({
|
||||||
variables: {
|
variables: {
|
||||||
email: emailElement.value,
|
email: emailElement.value,
|
||||||
password: passwordElement.value,
|
password: passwordElement.value,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
if (data.signIn.user) {
|
if (data.signIn.user) {
|
||||||
await router.push('/')
|
await router.push("/");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMsg(getErrorMessage(error))
|
setErrorMsg(getErrorMessage(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,11 +64,11 @@ function SignIn() {
|
||||||
required
|
required
|
||||||
label="Password"
|
label="Password"
|
||||||
/>
|
/>
|
||||||
<button type="submit">Sign in</button> or{' '}
|
<button type="submit">Sign in</button> or{" "}
|
||||||
<Link href="/signup">Sign up</Link>
|
<Link href="/signup">Sign up</Link>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SignIn
|
export default SignIn;
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from "react";
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
import { gql, useMutation, useApolloClient } from '@apollo/client'
|
import { gql, useMutation, useApolloClient } from "@apollo/client";
|
||||||
|
|
||||||
const SignOutMutation = gql`
|
const SignOutMutation = gql`
|
||||||
mutation SignOutMutation {
|
mutation SignOutMutation {
|
||||||
signOut
|
signOut
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
||||||
function SignOut() {
|
function SignOut() {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const [signOut] = useMutation(SignOutMutation)
|
const [signOut] = useMutation(SignOutMutation);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
signOut().then(() => {
|
signOut().then(() => {
|
||||||
client.resetStore().then(() => {
|
client.resetStore().then(() => {
|
||||||
router.push('/signin')
|
router.push("/signin");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}, [signOut, router, client])
|
}, [signOut, router, client]);
|
||||||
|
|
||||||
return <p>Signing out...</p>
|
return <p>Signing out...</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SignOut
|
export default SignOut;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useState } from 'react'
|
import { useState } from "react";
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { gql, useMutation } from "@apollo/client";
|
||||||
import { getErrorMessage } from '../lib/form'
|
import { getErrorMessage } from "../lib/form";
|
||||||
import Field from '../components/field'
|
import Field from "../components/field";
|
||||||
|
|
||||||
const SignUpMutation = gql`
|
const SignUpMutation = gql`
|
||||||
mutation SignUpMutation($email: String!, $password: String!) {
|
mutation SignUpMutation($email: String!, $password: String!) {
|
||||||
|
@ -14,17 +14,17 @@ const SignUpMutation = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
||||||
function SignUp() {
|
function SignUp() {
|
||||||
const [signUp] = useMutation(SignUpMutation)
|
const [signUp] = useMutation(SignUpMutation);
|
||||||
const [errorMsg, setErrorMsg] = useState()
|
const [errorMsg, setErrorMsg] = useState();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
async function handleSubmit(event) {
|
async function handleSubmit(event) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
const emailElement = event.currentTarget.elements.email
|
const emailElement = event.currentTarget.elements.email;
|
||||||
const passwordElement = event.currentTarget.elements.password
|
const passwordElement = event.currentTarget.elements.password;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await signUp({
|
await signUp({
|
||||||
|
@ -32,11 +32,11 @@ function SignUp() {
|
||||||
email: emailElement.value,
|
email: emailElement.value,
|
||||||
password: passwordElement.value,
|
password: passwordElement.value,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
router.push('/signin')
|
router.push("/signin");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMsg(getErrorMessage(error))
|
setErrorMsg(getErrorMessage(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,11 +59,11 @@ function SignUp() {
|
||||||
required
|
required
|
||||||
label="Password"
|
label="Password"
|
||||||
/>
|
/>
|
||||||
<button type="submit">Sign up</button> or{' '}
|
<button type="submit">Sign up</button> or{" "}
|
||||||
<Link href="/signin">Sign in</Link>
|
<Link href="/signin">Sign in</Link>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SignUp
|
export default SignUp;
|
||||||
|
|
|
@ -1,54 +1,54 @@
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from "react";
|
||||||
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
|
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
|
||||||
import { SchemaLink } from '@apollo/client/link/schema'
|
import { SchemaLink } from "@apollo/client/link/schema";
|
||||||
import { schema } from '../apollo/schema'
|
import { schema } from "../apollo/schema";
|
||||||
import merge from 'deepmerge'
|
import merge from "deepmerge";
|
||||||
|
|
||||||
let apolloClient
|
let apolloClient;
|
||||||
|
|
||||||
function createIsomorphLink() {
|
function createIsomorphLink() {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === "undefined") {
|
||||||
return new SchemaLink({ schema })
|
return new SchemaLink({ schema });
|
||||||
} else {
|
} else {
|
||||||
return new HttpLink({
|
return new HttpLink({
|
||||||
uri: '/api/graphql',
|
uri: "/api/graphql",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createApolloClient() {
|
function createApolloClient() {
|
||||||
return new ApolloClient({
|
return new ApolloClient({
|
||||||
ssrMode: typeof window === 'undefined',
|
ssrMode: typeof window === "undefined",
|
||||||
link: createIsomorphLink(),
|
link: createIsomorphLink(),
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializeApollo(initialState = null) {
|
export function initializeApollo(initialState = null) {
|
||||||
const _apolloClient = apolloClient ?? createApolloClient()
|
const _apolloClient = apolloClient ?? createApolloClient();
|
||||||
|
|
||||||
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
|
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
|
||||||
// gets hydrated here
|
// gets hydrated here
|
||||||
if (initialState) {
|
if (initialState) {
|
||||||
// Get existing cache, loaded during client side data fetching
|
// Get existing cache, loaded during client side data fetching
|
||||||
const existingCache = _apolloClient.extract()
|
const existingCache = _apolloClient.extract();
|
||||||
|
|
||||||
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
|
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
|
||||||
const data = merge(initialState, existingCache)
|
const data = merge(initialState, existingCache);
|
||||||
|
|
||||||
// Restore the cache with the merged data
|
// Restore the cache with the merged data
|
||||||
_apolloClient.cache.restore(data)
|
_apolloClient.cache.restore(data);
|
||||||
}
|
}
|
||||||
// For SSG and SSR always create a new Apollo Client
|
// For SSG and SSR always create a new Apollo Client
|
||||||
if (typeof window === 'undefined') return _apolloClient
|
if (typeof window === "undefined") return _apolloClient;
|
||||||
// Create the Apollo Client once in the client
|
// Create the Apollo Client once in the client
|
||||||
if (!apolloClient) apolloClient = _apolloClient
|
if (!apolloClient) apolloClient = _apolloClient;
|
||||||
|
|
||||||
return _apolloClient
|
return _apolloClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useApollo(initialState) {
|
export function useApollo(initialState) {
|
||||||
const store = useMemo(() => initializeApollo(initialState), [initialState])
|
const store = useMemo(() => initializeApollo(initialState), [initialState]);
|
||||||
return store
|
return store;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export const resolvers = {
|
export const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
viewer() {
|
viewer() {
|
||||||
return { id: 1, name: 'John Smith', status: 'cached' }
|
return { id: 1, name: "John Smith", status: "cached" };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { makeExecutableSchema } from '@graphql-tools/schema'
|
import { makeExecutableSchema } from "@graphql-tools/schema";
|
||||||
import { typeDefs } from './type-defs'
|
import { typeDefs } from "./type-defs";
|
||||||
import { resolvers } from './resolvers'
|
import { resolvers } from "./resolvers";
|
||||||
|
|
||||||
export const schema = makeExecutableSchema({
|
export const schema = makeExecutableSchema({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { gql } from '@apollo/client'
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
export const typeDefs = gql`
|
export const typeDefs = gql`
|
||||||
type User {
|
type User {
|
||||||
|
@ -10,4 +10,4 @@ export const typeDefs = gql`
|
||||||
type Query {
|
type Query {
|
||||||
viewer: User
|
viewer: User
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { ApolloProvider } from '@apollo/client'
|
import { ApolloProvider } from "@apollo/client";
|
||||||
import { useApollo } from '../apollo/client'
|
import { useApollo } from "../apollo/client";
|
||||||
|
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }) {
|
||||||
const apolloClient = useApollo(pageProps.initialApolloState)
|
const apolloClient = useApollo(pageProps.initialApolloState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={apolloClient}>
|
<ApolloProvider client={apolloClient}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
This is a static page goto <Link href="/">dynamic</Link> page.
|
This is a static page goto <Link href="/">dynamic</Link> page.
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ApolloServer } from '@apollo/server'
|
import { ApolloServer } from "@apollo/server";
|
||||||
import { startServerAndCreateNextHandler } from '@as-integrations/next'
|
import { startServerAndCreateNextHandler } from "@as-integrations/next";
|
||||||
import { schema } from '../../apollo/schema'
|
import { schema } from "../../apollo/schema";
|
||||||
|
|
||||||
const apolloServer = new ApolloServer({ schema })
|
const apolloServer = new ApolloServer({ schema });
|
||||||
|
|
||||||
export default startServerAndCreateNextHandler(apolloServer)
|
export default startServerAndCreateNextHandler(apolloServer);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import gql from 'graphql-tag'
|
import gql from "graphql-tag";
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from "@apollo/client";
|
||||||
import { initializeApollo } from '../apollo/client'
|
import { initializeApollo } from "../apollo/client";
|
||||||
|
|
||||||
const ViewerQuery = gql`
|
const ViewerQuery = gql`
|
||||||
query ViewerQuery {
|
query ViewerQuery {
|
||||||
|
@ -11,33 +11,33 @@ const ViewerQuery = gql`
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const {
|
const {
|
||||||
data: { viewer },
|
data: { viewer },
|
||||||
} = useQuery(ViewerQuery)
|
} = useQuery(ViewerQuery);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
You're signed in as {viewer.name} and you're {viewer.status} goto{' '}
|
You're signed in as {viewer.name} and you're {viewer.status} goto{" "}
|
||||||
<Link href="/about">static</Link> page.
|
<Link href="/about">static</Link> page.
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const apolloClient = initializeApollo()
|
const apolloClient = initializeApollo();
|
||||||
|
|
||||||
await apolloClient.query({
|
await apolloClient.query({
|
||||||
query: ViewerQuery,
|
query: ViewerQuery,
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
initialApolloState: apolloClient.cache.extract(),
|
initialApolloState: apolloClient.cache.extract(),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Index
|
export default Index;
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import queryGraphql from '../shared/query-graphql'
|
import queryGraphql from "../shared/query-graphql";
|
||||||
|
|
||||||
export default function UserProfile({ user }) {
|
export default function UserProfile({ user }) {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <h1>User Not Found</h1>
|
return <h1>User Not Found</h1>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<h1>
|
<h1>
|
||||||
{user.username} is {user.name}
|
{user.username} is {user.name}
|
||||||
</h1>
|
</h1>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticProps(context) {
|
export async function getStaticProps(context) {
|
||||||
const { params } = context
|
const { params } = context;
|
||||||
const { username } = params
|
const { username } = params;
|
||||||
const { user = null } = await queryGraphql(
|
const { user = null } = await queryGraphql(
|
||||||
`
|
`
|
||||||
query($username: String) {
|
query($username: String) {
|
||||||
|
@ -23,9 +23,9 @@ export async function getStaticProps(context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
{ username }
|
{ username },
|
||||||
)
|
);
|
||||||
return { props: { user } }
|
return { props: { user } };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
|
@ -35,12 +35,12 @@ export async function getStaticPaths() {
|
||||||
username
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)) as { users: { username: string }[] }
|
`)) as { users: { username: string }[] };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths: users.map(({ username }) => ({
|
paths: users.map(({ username }) => ({
|
||||||
params: { username },
|
params: { username },
|
||||||
})),
|
})),
|
||||||
fallback: true,
|
fallback: true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ApolloServer } from '@apollo/server'
|
import { ApolloServer } from "@apollo/server";
|
||||||
import { startServerAndCreateNextHandler } from '@as-integrations/next'
|
import { startServerAndCreateNextHandler } from "@as-integrations/next";
|
||||||
import { makeExecutableSchema } from '@graphql-tools/schema'
|
import { makeExecutableSchema } from "@graphql-tools/schema";
|
||||||
import { gql } from 'graphql-tag'
|
import { gql } from "graphql-tag";
|
||||||
|
|
||||||
const typeDefs = gql`
|
const typeDefs = gql`
|
||||||
type Query {
|
type Query {
|
||||||
|
@ -12,28 +12,28 @@ const typeDefs = gql`
|
||||||
name: String
|
name: String
|
||||||
username: String
|
username: String
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
||||||
const users = [
|
const users = [
|
||||||
{ name: 'Leeroy Jenkins', username: 'leeroy' },
|
{ name: "Leeroy Jenkins", username: "leeroy" },
|
||||||
{ name: 'Foo Bar', username: 'foobar' },
|
{ name: "Foo Bar", username: "foobar" },
|
||||||
]
|
];
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
users() {
|
users() {
|
||||||
return users
|
return users;
|
||||||
},
|
},
|
||||||
user(parent, { username }) {
|
user(parent, { username }) {
|
||||||
return users.find((user) => user.username === username)
|
return users.find((user) => user.username === username);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const schema = makeExecutableSchema({ typeDefs, resolvers })
|
export const schema = makeExecutableSchema({ typeDefs, resolvers });
|
||||||
|
|
||||||
const server = new ApolloServer({
|
const server = new ApolloServer({
|
||||||
schema,
|
schema,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default startServerAndCreateNextHandler(server)
|
export default startServerAndCreateNextHandler(server);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
|
|
||||||
import queryGraphql from '../shared/query-graphql'
|
import queryGraphql from "../shared/query-graphql";
|
||||||
|
|
||||||
export default function UserListing({ users }) {
|
export default function UserListing({ users }) {
|
||||||
return (
|
return (
|
||||||
|
@ -16,7 +16,7 @@ export default function UserListing({ users }) {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
|
@ -27,6 +27,6 @@ export async function getStaticProps() {
|
||||||
username
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)
|
`);
|
||||||
return { props: { users } }
|
return { props: { users } };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { graphql } from 'graphql'
|
import { graphql } from "graphql";
|
||||||
|
|
||||||
import { schema } from '../../pages/api/graphql'
|
import { schema } from "../../pages/api/graphql";
|
||||||
|
|
||||||
export default async function queryGraphql(query, variableValues = {}) {
|
export default async function queryGraphql(query, variableValues = {}) {
|
||||||
const { data } = await graphql({ schema, source: query, variableValues })
|
const { data } = await graphql({ schema, source: query, variableValues });
|
||||||
return data || {}
|
return data || {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,37 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import Cors from 'cors'
|
import Cors from "cors";
|
||||||
|
|
||||||
// Initializing the cors middleware
|
// Initializing the cors middleware
|
||||||
// You can read more about the available options here: https://github.com/expressjs/cors#configuration-options
|
// You can read more about the available options here: https://github.com/expressjs/cors#configuration-options
|
||||||
const cors = Cors({
|
const cors = Cors({
|
||||||
methods: ['POST', 'GET', 'HEAD'],
|
methods: ["POST", "GET", "HEAD"],
|
||||||
})
|
});
|
||||||
|
|
||||||
// Helper method to wait for a middleware to execute before continuing
|
// Helper method to wait for a middleware to execute before continuing
|
||||||
// And to throw an error when an error happens in a middleware
|
// And to throw an error when an error happens in a middleware
|
||||||
function runMiddleware(
|
function runMiddleware(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
fn: Function
|
fn: Function,
|
||||||
) {
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fn(req, res, (result: any) => {
|
fn(req, res, (result: any) => {
|
||||||
if (result instanceof Error) {
|
if (result instanceof Error) {
|
||||||
return reject(result)
|
return reject(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolve(result)
|
return resolve(result);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
// Run the middleware
|
// Run the middleware
|
||||||
await runMiddleware(req, res, cors)
|
await runMiddleware(req, res, cors);
|
||||||
|
|
||||||
// Rest of the API logic
|
// Rest of the API logic
|
||||||
res.json({ message: 'Hello Everyone!' })
|
res.json({ message: "Hello Everyone!" });
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,5 @@ export default function Index() {
|
||||||
domain and make a POST / GET / OPTIONS request to <b>/api/cors</b>. Using
|
domain and make a POST / GET / OPTIONS request to <b>/api/cors</b>. Using
|
||||||
a different method from those mentioned will be blocked by CORS
|
a different method from those mentioned will be blocked by CORS
|
||||||
</p>
|
</p>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createYoga, createSchema } from 'graphql-yoga'
|
import { createYoga, createSchema } from "graphql-yoga";
|
||||||
|
|
||||||
const typeDefs = /* GraphQL */ `
|
const typeDefs = /* GraphQL */ `
|
||||||
type Query {
|
type Query {
|
||||||
|
@ -7,30 +7,30 @@ const typeDefs = /* GraphQL */ `
|
||||||
type User {
|
type User {
|
||||||
name: String
|
name: String
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
users() {
|
users() {
|
||||||
return [{ name: 'Nextjs' }]
|
return [{ name: "Nextjs" }];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const schema = createSchema({
|
const schema = createSchema({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
// Disable body parsing (required for file uploads)
|
// Disable body parsing (required for file uploads)
|
||||||
bodyParser: false,
|
bodyParser: false,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default createYoga({
|
export default createYoga({
|
||||||
schema,
|
schema,
|
||||||
// Needed to be defined explicitly because our endpoint lives at a different path other than `/graphql`
|
// Needed to be defined explicitly because our endpoint lives at a different path other than `/graphql`
|
||||||
graphqlEndpoint: '/api/graphql',
|
graphqlEndpoint: "/api/graphql",
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,30 +1,33 @@
|
||||||
import useSWR from 'swr'
|
import useSWR from "swr";
|
||||||
|
|
||||||
const fetcher = (query: string) =>
|
const fetcher = (query: string) =>
|
||||||
fetch('/api/graphql', {
|
fetch("/api/graphql", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-type': 'application/json',
|
"Content-type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ query }),
|
body: JSON.stringify({ query }),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((json) => json.data)
|
.then((json) => json.data);
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
users: {
|
users: {
|
||||||
name: string
|
name: string;
|
||||||
}[]
|
}[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { data, error, isLoading } = useSWR<Data>('{ users { name } }', fetcher)
|
const { data, error, isLoading } = useSWR<Data>(
|
||||||
|
"{ users { name } }",
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
|
||||||
if (error) return <div>Failed to load</div>
|
if (error) return <div>Failed to load</div>;
|
||||||
if (isLoading) return <div>Loading...</div>
|
if (isLoading) return <div>Loading...</div>;
|
||||||
if (!data) return null
|
if (!data) return null;
|
||||||
|
|
||||||
const { users } = data
|
const { users } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -32,5 +35,5 @@ export default function Index() {
|
||||||
<div key={index}>{user.name}</div>
|
<div key={index}>{user.name}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { setCookie } from '../../utils/cookies'
|
import { setCookie } from "../../utils/cookies";
|
||||||
|
|
||||||
export default function handler(_req: NextApiRequest, res: NextApiResponse) {
|
export default function handler(_req: NextApiRequest, res: NextApiResponse) {
|
||||||
// Calling our pure function using the `res` object, it will add the `set-cookie` header
|
// Calling our pure function using the `res` object, it will add the `set-cookie` header
|
||||||
setCookie(res, 'Next.js', 'api-middleware!')
|
setCookie(res, "Next.js", "api-middleware!");
|
||||||
// Return the `set-cookie` header so we can display it in the browser and show that it works!
|
// Return the `set-cookie` header so we can display it in the browser and show that it works!
|
||||||
res.end(res.getHeader('Set-Cookie'))
|
res.end(res.getHeader("Set-Cookie"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import useSWR from 'swr'
|
import useSWR from "swr";
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((res) => res.text())
|
const fetcher = (url: string) => fetch(url).then((res) => res.text());
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { data, error, isLoading } = useSWR<string>('/api/cookies', fetcher)
|
const { data, error, isLoading } = useSWR<string>("/api/cookies", fetcher);
|
||||||
|
|
||||||
if (error) return <div>Failed to load</div>
|
if (error) return <div>Failed to load</div>;
|
||||||
if (isLoading) return <div>Loading...</div>
|
if (isLoading) return <div>Loading...</div>;
|
||||||
if (!data) return null
|
if (!data) return null;
|
||||||
|
|
||||||
return <div>{`Cookie from response: "${data}"`}</div>
|
return <div>{`Cookie from response: "${data}"`}</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { NextApiResponse } from 'next'
|
import type { NextApiResponse } from "next";
|
||||||
import { serialize, CookieSerializeOptions } from 'cookie'
|
import { serialize, CookieSerializeOptions } from "cookie";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This sets `cookie` using the `res` object
|
* This sets `cookie` using the `res` object
|
||||||
|
@ -9,14 +9,14 @@ export const setCookie = (
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
name: string,
|
name: string,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
options: CookieSerializeOptions = {}
|
options: CookieSerializeOptions = {},
|
||||||
) => {
|
) => {
|
||||||
const stringValue =
|
const stringValue =
|
||||||
typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
|
typeof value === "object" ? "j:" + JSON.stringify(value) : String(value);
|
||||||
|
|
||||||
if (typeof options.maxAge === 'number') {
|
if (typeof options.maxAge === "number") {
|
||||||
options.expires = new Date(Date.now() + options.maxAge * 1000)
|
options.expires = new Date(Date.now() + options.maxAge * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Set-Cookie', serialize(name, stringValue, options))
|
res.setHeader("Set-Cookie", serialize(name, stringValue, options));
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import rateLimit from '../../utils/rate-limit'
|
import rateLimit from "../../utils/rate-limit";
|
||||||
|
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
interval: 60 * 1000, // 60 seconds
|
interval: 60 * 1000, // 60 seconds
|
||||||
uniqueTokenPerInterval: 500, // Max 500 users per second
|
uniqueTokenPerInterval: 500, // Max 500 users per second
|
||||||
})
|
});
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
_req: NextApiRequest,
|
_req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await limiter.check(res, 10, 'CACHE_TOKEN') // 10 requests per minute
|
await limiter.check(res, 10, "CACHE_TOKEN"); // 10 requests per minute
|
||||||
res.status(200).json({ id: uuidv4() })
|
res.status(200).json({ id: uuidv4() });
|
||||||
} catch {
|
} catch {
|
||||||
res.status(429).json({ error: 'Rate limit exceeded' })
|
res.status(429).json({ error: "Rate limit exceeded" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
import { useState } from 'react'
|
import { useState } from "react";
|
||||||
import styles from '../styles.module.css'
|
import styles from "../styles.module.css";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const [response, setResponse] = useState<Record<string, unknown> | null>(null)
|
const [response, setResponse] = useState<Record<string, unknown> | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const makeRequest = async () => {
|
const makeRequest = async () => {
|
||||||
const res = await fetch('/api/user')
|
const res = await fetch("/api/user");
|
||||||
|
|
||||||
setResponse({
|
setResponse({
|
||||||
status: res.status,
|
status: res.status,
|
||||||
body: await res.json(),
|
body: await res.json(),
|
||||||
limit: res.headers.get('X-RateLimit-Limit'),
|
limit: res.headers.get("X-RateLimit-Limit"),
|
||||||
remaining: res.headers.get('X-RateLimit-Remaining'),
|
remaining: res.headers.get("X-RateLimit-Remaining"),
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.container}>
|
<main className={styles.container}>
|
||||||
<h1>Next.js API Routes Rate Limiting</h1>
|
<h1>Next.js API Routes Rate Limiting</h1>
|
||||||
<p>
|
<p>
|
||||||
This example uses <code className={styles.inlineCode}>lru-cache</code>{' '}
|
This example uses <code className={styles.inlineCode}>lru-cache</code>{" "}
|
||||||
to implement a simple rate limiter for API routes (Serverless
|
to implement a simple rate limiter for API routes (Serverless
|
||||||
Functions).
|
Functions).
|
||||||
</p>
|
</p>
|
||||||
|
@ -30,5 +32,5 @@ export default function Index() {
|
||||||
</code>
|
</code>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
|
|
||||||
.inlineCode::before,
|
.inlineCode::before,
|
||||||
.inlineCode::after {
|
.inlineCode::after {
|
||||||
content: '`';
|
content: "`";
|
||||||
}
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
import type { NextApiResponse } from 'next'
|
import type { NextApiResponse } from "next";
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from "lru-cache";
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
uniqueTokenPerInterval?: number
|
uniqueTokenPerInterval?: number;
|
||||||
interval?: number
|
interval?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function rateLimit(options?: Options) {
|
export default function rateLimit(options?: Options) {
|
||||||
const tokenCache = new LRUCache({
|
const tokenCache = new LRUCache({
|
||||||
max: options?.uniqueTokenPerInterval || 500,
|
max: options?.uniqueTokenPerInterval || 500,
|
||||||
ttl: options?.interval || 60000,
|
ttl: options?.interval || 60000,
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
check: (res: NextApiResponse, limit: number, token: string) =>
|
check: (res: NextApiResponse, limit: number, token: string) =>
|
||||||
new Promise<void>((resolve, reject) => {
|
new Promise<void>((resolve, reject) => {
|
||||||
const tokenCount = (tokenCache.get(token) as number[]) || [0]
|
const tokenCount = (tokenCache.get(token) as number[]) || [0];
|
||||||
if (tokenCount[0] === 0) {
|
if (tokenCount[0] === 0) {
|
||||||
tokenCache.set(token, tokenCount)
|
tokenCache.set(token, tokenCount);
|
||||||
}
|
}
|
||||||
tokenCount[0] += 1
|
tokenCount[0] += 1;
|
||||||
|
|
||||||
const currentUsage = tokenCount[0]
|
const currentUsage = tokenCount[0];
|
||||||
const isRateLimited = currentUsage >= limit
|
const isRateLimited = currentUsage >= limit;
|
||||||
res.setHeader('X-RateLimit-Limit', limit)
|
res.setHeader("X-RateLimit-Limit", limit);
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
'X-RateLimit-Remaining',
|
"X-RateLimit-Remaining",
|
||||||
isRateLimited ? 0 : limit - currentUsage
|
isRateLimited ? 0 : limit - currentUsage,
|
||||||
)
|
);
|
||||||
|
|
||||||
return isRateLimited ? reject() : resolve()
|
return isRateLimited ? reject() : resolve();
|
||||||
}),
|
}),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number
|
id: number;
|
||||||
name?: string
|
name?: string;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import type { User } from '../../../interfaces'
|
import type { User } from "../../../interfaces";
|
||||||
|
|
||||||
export default function userHandler(
|
export default function userHandler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<User>
|
res: NextApiResponse<User>,
|
||||||
) {
|
) {
|
||||||
const { query, method } = req
|
const { query, method } = req;
|
||||||
const id = parseInt(query.id as string, 10)
|
const id = parseInt(query.id as string, 10);
|
||||||
const name = query.name as string
|
const name = query.name as string;
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'GET':
|
case "GET":
|
||||||
// Get data from your database
|
// Get data from your database
|
||||||
res.status(200).json({ id, name: `User ${id}` })
|
res.status(200).json({ id, name: `User ${id}` });
|
||||||
break
|
break;
|
||||||
case 'PUT':
|
case "PUT":
|
||||||
// Update or create data in your database
|
// Update or create data in your database
|
||||||
res.status(200).json({ id, name: name || `User ${id}` })
|
res.status(200).json({ id, name: name || `User ${id}` });
|
||||||
break
|
break;
|
||||||
default:
|
default:
|
||||||
res.setHeader('Allow', ['GET', 'PUT'])
|
res.setHeader("Allow", ["GET", "PUT"]);
|
||||||
res.status(405).end(`Method ${method} Not Allowed`)
|
res.status(405).end(`Method ${method} Not Allowed`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import type { User } from '../../interfaces'
|
import type { User } from "../../interfaces";
|
||||||
|
|
||||||
// Fake users data
|
// Fake users data
|
||||||
const users: User[] = [{ id: 1 }, { id: 2 }, { id: 3 }]
|
const users: User[] = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
||||||
|
|
||||||
export default function handler(
|
export default function handler(
|
||||||
_req: NextApiRequest,
|
_req: NextApiRequest,
|
||||||
res: NextApiResponse<User[]>
|
res: NextApiResponse<User[]>,
|
||||||
) {
|
) {
|
||||||
// Get data from your database
|
// Get data from your database
|
||||||
res.status(200).json(users)
|
res.status(200).json(users);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import type { User } from '../interfaces'
|
import type { User } from "../interfaces";
|
||||||
import useSwr from 'swr'
|
import useSwr from "swr";
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { data, error, isLoading } = useSwr<User[]>('/api/users', fetcher)
|
const { data, error, isLoading } = useSwr<User[]>("/api/users", fetcher);
|
||||||
|
|
||||||
if (error) return <div>Failed to load users</div>
|
if (error) return <div>Failed to load users</div>;
|
||||||
if (isLoading) return <div>Loading...</div>
|
if (isLoading) return <div>Loading...</div>;
|
||||||
if (!data) return null
|
if (!data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -21,5 +21,5 @@ export default function Index() {
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import type { User } from '../../interfaces'
|
import type { User } from "../../interfaces";
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
import useSwr from 'swr'
|
import useSwr from "swr";
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
export default function UserPage() {
|
export default function UserPage() {
|
||||||
const { query } = useRouter()
|
const { query } = useRouter();
|
||||||
const { data, error, isLoading } = useSwr<User>(
|
const { data, error, isLoading } = useSwr<User>(
|
||||||
query.id ? `/api/user/${query.id}` : null,
|
query.id ? `/api/user/${query.id}` : null,
|
||||||
fetcher
|
fetcher,
|
||||||
)
|
);
|
||||||
|
|
||||||
if (error) return <div>Failed to load user</div>
|
if (error) return <div>Failed to load user</div>;
|
||||||
if (isLoading) return <div>Loading...</div>
|
if (isLoading) return <div>Loading...</div>;
|
||||||
if (!data) return null
|
if (!data) return null;
|
||||||
|
|
||||||
return <div>{data.name}</div>
|
return <div>{data.name}</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import { Person } from '../interfaces'
|
import { Person } from "../interfaces";
|
||||||
|
|
||||||
type PersonProps = {
|
type PersonProps = {
|
||||||
person: Person
|
person: Person;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function PersonComponent({ person }: PersonProps) {
|
export default function PersonComponent({ person }: PersonProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -12,5 +12,5 @@ export default function PersonComponent({ person }: PersonProps) {
|
||||||
{person.name}
|
{person.name}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,102 +1,102 @@
|
||||||
export const people = [
|
export const people = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: "1",
|
||||||
name: 'Luke Skywalker',
|
name: "Luke Skywalker",
|
||||||
height: '172',
|
height: "172",
|
||||||
mass: '77',
|
mass: "77",
|
||||||
hair_color: 'blond',
|
hair_color: "blond",
|
||||||
skin_color: 'fair',
|
skin_color: "fair",
|
||||||
eye_color: 'blue',
|
eye_color: "blue",
|
||||||
gender: 'male',
|
gender: "male",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: "2",
|
||||||
name: 'C-3PO',
|
name: "C-3PO",
|
||||||
height: '167',
|
height: "167",
|
||||||
mass: '75',
|
mass: "75",
|
||||||
hair_color: 'n/a',
|
hair_color: "n/a",
|
||||||
skin_color: 'gold',
|
skin_color: "gold",
|
||||||
eye_color: 'yellow',
|
eye_color: "yellow",
|
||||||
gender: 'n/a',
|
gender: "n/a",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: "3",
|
||||||
name: 'R2-D2',
|
name: "R2-D2",
|
||||||
height: '96',
|
height: "96",
|
||||||
mass: '32',
|
mass: "32",
|
||||||
hair_color: 'n/a',
|
hair_color: "n/a",
|
||||||
skin_color: 'white, blue',
|
skin_color: "white, blue",
|
||||||
eye_color: 'red',
|
eye_color: "red",
|
||||||
gender: 'n/a',
|
gender: "n/a",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: "4",
|
||||||
name: 'Darth Vader',
|
name: "Darth Vader",
|
||||||
height: '202',
|
height: "202",
|
||||||
mass: '136',
|
mass: "136",
|
||||||
hair_color: 'none',
|
hair_color: "none",
|
||||||
skin_color: 'white',
|
skin_color: "white",
|
||||||
eye_color: 'yellow',
|
eye_color: "yellow",
|
||||||
gender: 'male',
|
gender: "male",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '5',
|
id: "5",
|
||||||
name: 'Leia Organa',
|
name: "Leia Organa",
|
||||||
height: '150',
|
height: "150",
|
||||||
mass: '49',
|
mass: "49",
|
||||||
hair_color: 'brown',
|
hair_color: "brown",
|
||||||
skin_color: 'light',
|
skin_color: "light",
|
||||||
eye_color: 'brown',
|
eye_color: "brown",
|
||||||
gender: 'female',
|
gender: "female",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '6',
|
id: "6",
|
||||||
name: 'Owen Lars',
|
name: "Owen Lars",
|
||||||
height: '178',
|
height: "178",
|
||||||
mass: '120',
|
mass: "120",
|
||||||
hair_color: 'brown, grey',
|
hair_color: "brown, grey",
|
||||||
skin_color: 'light',
|
skin_color: "light",
|
||||||
eye_color: 'blue',
|
eye_color: "blue",
|
||||||
gender: 'male',
|
gender: "male",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '7',
|
id: "7",
|
||||||
name: 'Beru Whitesun Lars',
|
name: "Beru Whitesun Lars",
|
||||||
height: '165',
|
height: "165",
|
||||||
mass: '75',
|
mass: "75",
|
||||||
hair_color: 'brown',
|
hair_color: "brown",
|
||||||
skin_color: 'light',
|
skin_color: "light",
|
||||||
eye_color: 'blue',
|
eye_color: "blue",
|
||||||
gender: 'female',
|
gender: "female",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '8',
|
id: "8",
|
||||||
name: 'R5-D4',
|
name: "R5-D4",
|
||||||
height: '97',
|
height: "97",
|
||||||
mass: '32',
|
mass: "32",
|
||||||
hair_color: 'n/a',
|
hair_color: "n/a",
|
||||||
skin_color: 'white, red',
|
skin_color: "white, red",
|
||||||
eye_color: 'red',
|
eye_color: "red",
|
||||||
gender: 'n/a',
|
gender: "n/a",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '9',
|
id: "9",
|
||||||
name: 'Biggs Darklighter',
|
name: "Biggs Darklighter",
|
||||||
height: '183',
|
height: "183",
|
||||||
mass: '84',
|
mass: "84",
|
||||||
hair_color: 'black',
|
hair_color: "black",
|
||||||
skin_color: 'light',
|
skin_color: "light",
|
||||||
eye_color: 'brown',
|
eye_color: "brown",
|
||||||
gender: 'male',
|
gender: "male",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '10',
|
id: "10",
|
||||||
name: 'Obi-Wan Kenobi',
|
name: "Obi-Wan Kenobi",
|
||||||
height: '182',
|
height: "182",
|
||||||
mass: '77',
|
mass: "77",
|
||||||
hair_color: 'auburn, white',
|
hair_color: "auburn, white",
|
||||||
skin_color: 'fair',
|
skin_color: "fair",
|
||||||
eye_color: 'blue-gray',
|
eye_color: "blue-gray",
|
||||||
gender: 'male',
|
gender: "male",
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
export type Person = {
|
export type Person = {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
height: string
|
height: string;
|
||||||
mass: string
|
mass: string;
|
||||||
hair_color: string
|
hair_color: string;
|
||||||
skin_color: string
|
skin_color: string;
|
||||||
eye_color: string
|
eye_color: string;
|
||||||
gender: string
|
gender: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ResponseError = {
|
export type ResponseError = {
|
||||||
message: string
|
message: string;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { people } from '../../../data'
|
import { people } from "../../../data";
|
||||||
import type { Person, ResponseError } from '../../../interfaces'
|
import type { Person, ResponseError } from "../../../interfaces";
|
||||||
|
|
||||||
export default function personHandler(
|
export default function personHandler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<Person | ResponseError>
|
res: NextApiResponse<Person | ResponseError>,
|
||||||
) {
|
) {
|
||||||
const { query } = req
|
const { query } = req;
|
||||||
const { id } = query
|
const { id } = query;
|
||||||
const person = people.find((p) => p.id === id)
|
const person = people.find((p) => p.id === id);
|
||||||
|
|
||||||
// User with id exists
|
// User with id exists
|
||||||
return person
|
return person
|
||||||
? res.status(200).json(person)
|
? res.status(200).json(person)
|
||||||
: res.status(404).json({ message: `User with id: ${id} not found.` })
|
: res.status(404).json({ message: `User with id: ${id} not found.` });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { NextApiResponse, NextApiRequest } from 'next'
|
import { NextApiResponse, NextApiRequest } from "next";
|
||||||
import { people } from '../../../data'
|
import { people } from "../../../data";
|
||||||
import { Person } from '../../../interfaces'
|
import { Person } from "../../../interfaces";
|
||||||
|
|
||||||
export default function handler(
|
export default function handler(
|
||||||
_req: NextApiRequest,
|
_req: NextApiRequest,
|
||||||
res: NextApiResponse<Person[]>
|
res: NextApiResponse<Person[]>,
|
||||||
) {
|
) {
|
||||||
return res.status(200).json(people)
|
return res.status(200).json(people);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import useSWR from 'swr'
|
import useSWR from "swr";
|
||||||
import PersonComponent from '../components/Person'
|
import PersonComponent from "../components/Person";
|
||||||
import type { Person } from '../interfaces'
|
import type { Person } from "../interfaces";
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { data, error, isLoading } = useSWR<Person[]>('/api/people', fetcher)
|
const { data, error, isLoading } = useSWR<Person[]>("/api/people", fetcher);
|
||||||
|
|
||||||
if (error) return <div>Failed to load</div>
|
if (error) return <div>Failed to load</div>;
|
||||||
if (isLoading) return <div>Loading...</div>
|
if (isLoading) return <div>Loading...</div>;
|
||||||
if (!data) return null
|
if (!data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -17,5 +17,5 @@ export default function Index() {
|
||||||
<PersonComponent key={p.id} person={p} />
|
<PersonComponent key={p.id} person={p} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
import useSWR from 'swr'
|
import useSWR from "swr";
|
||||||
import type { Person, ResponseError } from '../../interfaces'
|
import type { Person, ResponseError } from "../../interfaces";
|
||||||
|
|
||||||
const fetcher = async (url: string) => {
|
const fetcher = async (url: string) => {
|
||||||
const res = await fetch(url)
|
const res = await fetch(url);
|
||||||
const data = await res.json()
|
const data = await res.json();
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error(data.message)
|
throw new Error(data.message);
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export default function PersonPage() {
|
export default function PersonPage() {
|
||||||
const { query } = useRouter()
|
const { query } = useRouter();
|
||||||
const { data, error, isLoading, isValidating } = useSWR<
|
const { data, error, isLoading, isValidating } = useSWR<
|
||||||
Person,
|
Person,
|
||||||
ResponseError
|
ResponseError
|
||||||
>(() => (query.id ? `/api/people/${query.id}` : null), fetcher)
|
>(() => (query.id ? `/api/people/${query.id}` : null), fetcher);
|
||||||
|
|
||||||
if (error) return <div>{error.message}</div>
|
if (error) return <div>{error.message}</div>;
|
||||||
if (isLoading) return <div>Loading...</div>
|
if (isLoading) return <div>Loading...</div>;
|
||||||
if (!data) return null
|
if (!data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
|
@ -56,5 +56,5 @@ export default function PersonPage() {
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from "react";
|
||||||
import { type getDictionary } from '../../../get-dictionary'
|
import { type getDictionary } from "../../../get-dictionary";
|
||||||
|
|
||||||
export default function Counter({
|
export default function Counter({
|
||||||
dictionary,
|
dictionary,
|
||||||
}: {
|
}: {
|
||||||
dictionary: Awaited<ReturnType<typeof getDictionary>>['counter']
|
dictionary: Awaited<ReturnType<typeof getDictionary>>["counter"];
|
||||||
}) {
|
}) {
|
||||||
const [count, setCount] = useState(0)
|
const [count, setCount] = useState(0);
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
This component is rendered on client:
|
This component is rendered on client:
|
||||||
|
@ -20,5 +20,5 @@ export default function Counter({
|
||||||
{dictionary.increment}
|
{dictionary.increment}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from "next/navigation";
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import { i18n, type Locale } from '../../../i18n-config'
|
import { i18n, type Locale } from "../../../i18n-config";
|
||||||
|
|
||||||
export default function LocaleSwitcher() {
|
export default function LocaleSwitcher() {
|
||||||
const pathName = usePathname()
|
const pathName = usePathname();
|
||||||
const redirectedPathName = (locale: Locale) => {
|
const redirectedPathName = (locale: Locale) => {
|
||||||
if (!pathName) return '/'
|
if (!pathName) return "/";
|
||||||
const segments = pathName.split('/')
|
const segments = pathName.split("/");
|
||||||
segments[1] = locale
|
segments[1] = locale;
|
||||||
return segments.join('/')
|
return segments.join("/");
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -22,9 +22,9 @@ export default function LocaleSwitcher() {
|
||||||
<li key={locale}>
|
<li key={locale}>
|
||||||
<Link href={redirectedPathName(locale)}>{locale}</Link>
|
<Link href={redirectedPathName(locale)}>{locale}</Link>
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import { i18n, type Locale } from '../../i18n-config'
|
import { i18n, type Locale } from "../../i18n-config";
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
return i18n.locales.map((locale) => ({ lang: locale }))
|
return i18n.locales.map((locale) => ({ lang: locale }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Root({
|
export default function Root({
|
||||||
children,
|
children,
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
params: { lang: Locale }
|
params: { lang: Locale };
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang={params.lang}>
|
<html lang={params.lang}>
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'i18n within app directory - Vercel Examples',
|
title: "i18n within app directory - Vercel Examples",
|
||||||
description: 'How to do i18n in Next.js 13 within app directory',
|
description: "How to do i18n in Next.js 13 within app directory",
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import { getDictionary } from '../../get-dictionary'
|
import { getDictionary } from "../../get-dictionary";
|
||||||
import { Locale } from '../../i18n-config'
|
import { Locale } from "../../i18n-config";
|
||||||
import Counter from './components/counter'
|
import Counter from "./components/counter";
|
||||||
import LocaleSwitcher from './components/locale-switcher'
|
import LocaleSwitcher from "./components/locale-switcher";
|
||||||
|
|
||||||
export default async function IndexPage({
|
export default async function IndexPage({
|
||||||
params: { lang },
|
params: { lang },
|
||||||
}: {
|
}: {
|
||||||
params: { lang: Locale }
|
params: { lang: Locale };
|
||||||
}) {
|
}) {
|
||||||
const dictionary = await getDictionary(lang)
|
const dictionary = await getDictionary(lang);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LocaleSwitcher />
|
<LocaleSwitcher />
|
||||||
<p>Current locale: {lang}</p>
|
<p>Current locale: {lang}</p>
|
||||||
<p>
|
<p>
|
||||||
This text is rendered on the server:{' '}
|
This text is rendered on the server:{" "}
|
||||||
{dictionary['server-component'].welcome}
|
{dictionary["server-component"].welcome}
|
||||||
</p>
|
</p>
|
||||||
<Counter dictionary={dictionary.counter} />
|
<Counter dictionary={dictionary.counter} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import 'server-only'
|
import "server-only";
|
||||||
import type { Locale } from './i18n-config'
|
import type { Locale } from "./i18n-config";
|
||||||
|
|
||||||
// We enumerate all dictionaries here for better linting and typescript support
|
// We enumerate all dictionaries here for better linting and typescript support
|
||||||
// We also get the default import for cleaner types
|
// We also get the default import for cleaner types
|
||||||
const dictionaries = {
|
const dictionaries = {
|
||||||
en: () => import('./dictionaries/en.json').then((module) => module.default),
|
en: () => import("./dictionaries/en.json").then((module) => module.default),
|
||||||
de: () => import('./dictionaries/de.json').then((module) => module.default),
|
de: () => import("./dictionaries/de.json").then((module) => module.default),
|
||||||
cs: () => import('./dictionaries/cs.json').then((module) => module.default),
|
cs: () => import("./dictionaries/cs.json").then((module) => module.default),
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getDictionary = async (locale: Locale) =>
|
export const getDictionary = async (locale: Locale) =>
|
||||||
dictionaries[locale]?.() ?? dictionaries.en()
|
dictionaries[locale]?.() ?? dictionaries.en();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export const i18n = {
|
export const i18n = {
|
||||||
defaultLocale: 'en',
|
defaultLocale: "en",
|
||||||
locales: ['en', 'de', 'cs'],
|
locales: ["en", "de", "cs"],
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type Locale = (typeof i18n)['locales'][number]
|
export type Locale = (typeof i18n)["locales"][number];
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from "next/server";
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
import { i18n } from './i18n-config'
|
import { i18n } from "./i18n-config";
|
||||||
|
|
||||||
import { match as matchLocale } from '@formatjs/intl-localematcher'
|
import { match as matchLocale } from "@formatjs/intl-localematcher";
|
||||||
import Negotiator from 'negotiator'
|
import Negotiator from "negotiator";
|
||||||
|
|
||||||
function getLocale(request: NextRequest): string | undefined {
|
function getLocale(request: NextRequest): string | undefined {
|
||||||
// Negotiator expects plain object so we need to transform headers
|
// Negotiator expects plain object so we need to transform headers
|
||||||
const negotiatorHeaders: Record<string, string> = {}
|
const negotiatorHeaders: Record<string, string> = {};
|
||||||
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value))
|
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
|
||||||
|
|
||||||
// @ts-ignore locales are readonly
|
// @ts-ignore locales are readonly
|
||||||
const locales: string[] = i18n.locales
|
const locales: string[] = i18n.locales;
|
||||||
|
|
||||||
// Use negotiator and intl-localematcher to get best locale
|
// Use negotiator and intl-localematcher to get best locale
|
||||||
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(
|
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(
|
||||||
locales
|
locales,
|
||||||
)
|
);
|
||||||
|
|
||||||
const locale = matchLocale(languages, locales, i18n.defaultLocale)
|
const locale = matchLocale(languages, locales, i18n.defaultLocale);
|
||||||
|
|
||||||
return locale
|
return locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
export function middleware(request: NextRequest) {
|
||||||
const pathname = request.nextUrl.pathname
|
const pathname = request.nextUrl.pathname;
|
||||||
|
|
||||||
// // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
|
// // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
|
||||||
// // If you have one
|
// // If you have one
|
||||||
|
@ -40,25 +40,26 @@ export function middleware(request: NextRequest) {
|
||||||
|
|
||||||
// Check if there is any supported locale in the pathname
|
// Check if there is any supported locale in the pathname
|
||||||
const pathnameIsMissingLocale = i18n.locales.every(
|
const pathnameIsMissingLocale = i18n.locales.every(
|
||||||
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
|
(locale) =>
|
||||||
)
|
!pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
|
||||||
|
);
|
||||||
|
|
||||||
// Redirect if there is no locale
|
// Redirect if there is no locale
|
||||||
if (pathnameIsMissingLocale) {
|
if (pathnameIsMissingLocale) {
|
||||||
const locale = getLocale(request)
|
const locale = getLocale(request);
|
||||||
|
|
||||||
// e.g. incoming request is /products
|
// e.g. incoming request is /products
|
||||||
// The new URL is now /en-US/products
|
// The new URL is now /en-US/products
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL(
|
new URL(
|
||||||
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
|
`/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
|
||||||
request.url
|
request.url,
|
||||||
)
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
// Matcher ignoring `/_next/` and `/api/`
|
// Matcher ignoring `/_next/` and `/api/`
|
||||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
:root {
|
:root {
|
||||||
--max-width: 1100px;
|
--max-width: 1100px;
|
||||||
--border-radius: 12px;
|
--border-radius: 12px;
|
||||||
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
|
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
|
||||||
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
|
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
|
||||||
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
|
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||||
|
|
||||||
--foreground-rgb: 0, 0, 0;
|
--foreground-rgb: 0, 0, 0;
|
||||||
--background-start-rgb: 214, 219, 220;
|
--background-start-rgb: 214, 219, 220;
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import './globals.css'
|
import "./globals.css";
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Create Next App',
|
title: "Create Next App",
|
||||||
description: 'Generated by create next app',
|
description: "Generated by create next app",
|
||||||
}
|
};
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
|
|
||||||
.center::before,
|
.center::before,
|
||||||
.center::after {
|
.center::after {
|
||||||
content: '';
|
content: "";
|
||||||
left: 50%;
|
left: 50%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
filter: blur(45px);
|
filter: blur(45px);
|
||||||
|
@ -130,7 +130,7 @@
|
||||||
|
|
||||||
.thirteen::before,
|
.thirteen::before,
|
||||||
.thirteen::after {
|
.thirteen::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Image from 'next/image'
|
import Image from "next/image";
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from "next/font/google";
|
||||||
import styles from './page.module.css'
|
import styles from "./page.module.css";
|
||||||
import Content from './message.mdx'
|
import Content from "./message.mdx";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
|
@ -19,7 +19,7 @@ export default function Home() {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
By{' '}
|
By{" "}
|
||||||
<Image
|
<Image
|
||||||
src="/vercel.svg"
|
src="/vercel.svg"
|
||||||
alt="Vercel Logo"
|
alt="Vercel Logo"
|
||||||
|
@ -78,5 +78,5 @@ export default function Home() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { MDXComponents } from 'mdx/types'
|
import type { MDXComponents } from "mdx/types";
|
||||||
|
|
||||||
// This file is required to use MDX in `app` directory.
|
// This file is required to use MDX in `app` directory.
|
||||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||||
|
@ -6,5 +6,5 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||||
// Allows customizing built-in components, e.g. to add styling.
|
// Allows customizing built-in components, e.g. to add styling.
|
||||||
// h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
|
// h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
|
||||||
...components,
|
...components,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
|
pageExtensions: ["js", "jsx", "ts", "tsx", "mdx"],
|
||||||
experimental: {
|
experimental: {
|
||||||
mdxRs: true,
|
mdxRs: true,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const withMDX = require('@next/mdx')()
|
const withMDX = require("@next/mdx")();
|
||||||
module.exports = withMDX(nextConfig)
|
module.exports = withMDX(nextConfig);
|
||||||
|
|
6
examples/app-dir-mdx/types/mdx.d.ts
vendored
6
examples/app-dir-mdx/types/mdx.d.ts
vendored
|
@ -1,5 +1,5 @@
|
||||||
// types/mdx.d.ts
|
// types/mdx.d.ts
|
||||||
declare module '*.mdx' {
|
declare module "*.mdx" {
|
||||||
let MDXComponent: (props) => JSX.Element
|
let MDXComponent: (props) => JSX.Element;
|
||||||
export default MDXComponent
|
export default MDXComponent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
import styles from '../styles/Home.module.css'
|
import styles from "../styles/Home.module.css";
|
||||||
import { LoginMethod } from '../lib/types'
|
import { LoginMethod } from "../lib/types";
|
||||||
import StytchContainer from './StytchContainer'
|
import StytchContainer from "./StytchContainer";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setLoginMethod: (loginMethod: LoginMethod) => void
|
setLoginMethod: (loginMethod: LoginMethod) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const LoginEntryPoint = (props: Props) => {
|
const LoginEntryPoint = (props: Props) => {
|
||||||
const { setLoginMethod } = props
|
const { setLoginMethod } = props;
|
||||||
return (
|
return (
|
||||||
<StytchContainer>
|
<StytchContainer>
|
||||||
<h2>Hello Vercel!</h2>
|
<h2>Hello Vercel!</h2>
|
||||||
|
@ -29,7 +29,7 @@ const LoginEntryPoint = (props: Props) => {
|
||||||
API Integration (SMS Passcodes)
|
API Integration (SMS Passcodes)
|
||||||
</button>
|
</button>
|
||||||
</StytchContainer>
|
</StytchContainer>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LoginEntryPoint
|
export default LoginEntryPoint;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from "react";
|
||||||
import SendOTPForm from './SendOTPForm'
|
import SendOTPForm from "./SendOTPForm";
|
||||||
import VerifyOTPForm from './VerifyOTPForm'
|
import VerifyOTPForm from "./VerifyOTPForm";
|
||||||
import StytchContainer from './StytchContainer'
|
import StytchContainer from "./StytchContainer";
|
||||||
|
|
||||||
const LoginWithSMS = () => {
|
const LoginWithSMS = () => {
|
||||||
const [otpSent, setOTPSent] = useState(false)
|
const [otpSent, setOTPSent] = useState(false);
|
||||||
const [phoneNumber, setPhoneNumber] = useState('')
|
const [phoneNumber, setPhoneNumber] = useState("");
|
||||||
const [methodId, setMethodId] = useState('')
|
const [methodId, setMethodId] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StytchContainer>
|
<StytchContainer>
|
||||||
|
@ -21,7 +21,7 @@ const LoginWithSMS = () => {
|
||||||
<VerifyOTPForm methodId={methodId} phoneNumber={phoneNumber} />
|
<VerifyOTPForm methodId={methodId} phoneNumber={phoneNumber} />
|
||||||
)}
|
)}
|
||||||
</StytchContainer>
|
</StytchContainer>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LoginWithSMS
|
export default LoginWithSMS;
|
||||||
|
|
|
@ -1,44 +1,44 @@
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
import { sendOTP } from '../lib/otpUtils'
|
import { sendOTP } from "../lib/otpUtils";
|
||||||
import styles from '../styles/Home.module.css'
|
import styles from "../styles/Home.module.css";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
phoneNumber: string
|
phoneNumber: string;
|
||||||
setMethodId: (methodId: string) => void
|
setMethodId: (methodId: string) => void;
|
||||||
setOTPSent: (submitted: boolean) => void
|
setOTPSent: (submitted: boolean) => void;
|
||||||
setPhoneNumber: (phoneNumber: string) => void
|
setPhoneNumber: (phoneNumber: string) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const SendOTPForm = (props: Props): JSX.Element => {
|
const SendOTPForm = (props: Props): JSX.Element => {
|
||||||
const { phoneNumber, setMethodId, setOTPSent, setPhoneNumber } = props
|
const { phoneNumber, setMethodId, setOTPSent, setPhoneNumber } = props;
|
||||||
const [isDisabled, setIsDisabled] = React.useState(true)
|
const [isDisabled, setIsDisabled] = React.useState(true);
|
||||||
|
|
||||||
const isValidNumber = (phoneNumberValue: string) => {
|
const isValidNumber = (phoneNumberValue: string) => {
|
||||||
// Regex validates phone numbers in (xxx)xxx-xxxx, xxx-xxx-xxxx, xxxxxxxxxx, and xxx.xxx.xxxx format
|
// Regex validates phone numbers in (xxx)xxx-xxxx, xxx-xxx-xxxx, xxxxxxxxxx, and xxx.xxx.xxxx format
|
||||||
const regex = /^[(]?[0-9]{3}[)]?[-s.]?[0-9]{3}[-s.]?[0-9]{4}$/g
|
const regex = /^[(]?[0-9]{3}[)]?[-s.]?[0-9]{3}[-s.]?[0-9]{4}$/g;
|
||||||
if (phoneNumberValue.match(regex)) {
|
if (phoneNumberValue.match(regex)) {
|
||||||
return true
|
return true;
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const onPhoneNumberChange = (e: React.ChangeEvent<{ value: string }>) => {
|
const onPhoneNumberChange = (e: React.ChangeEvent<{ value: string }>) => {
|
||||||
setPhoneNumber(e.target.value)
|
setPhoneNumber(e.target.value);
|
||||||
if (isValidNumber(e.target.value)) {
|
if (isValidNumber(e.target.value)) {
|
||||||
setIsDisabled(false)
|
setIsDisabled(false);
|
||||||
} else {
|
} else {
|
||||||
setIsDisabled(true)
|
setIsDisabled(true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent) => {
|
const onSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
if (isValidNumber(phoneNumber)) {
|
if (isValidNumber(phoneNumber)) {
|
||||||
const methodId = await sendOTP(phoneNumber)
|
const methodId = await sendOTP(phoneNumber);
|
||||||
setMethodId(methodId)
|
setMethodId(methodId);
|
||||||
setOTPSent(true)
|
setOTPSent(true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -77,7 +77,7 @@ const SendOTPForm = (props: Props): JSX.Element => {
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SendOTPForm
|
export default SendOTPForm;
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
import styles from '../styles/Home.module.css'
|
import styles from "../styles/Home.module.css";
|
||||||
import Image from 'next/image'
|
import Image from "next/image";
|
||||||
import lockup from '/public/powered-by-stytch.svg'
|
import lockup from "/public/powered-by-stytch.svg";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactElement | React.ReactElement[]
|
children: React.ReactElement | React.ReactElement[];
|
||||||
}
|
};
|
||||||
|
|
||||||
const StytchContainer = (props: Props) => {
|
const StytchContainer = (props: Props) => {
|
||||||
const { children } = props
|
const { children } = props;
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
<Image alt="Powered by Stytch" height={15} src={lockup} width={150} />
|
<Image alt="Powered by Stytch" height={15} src={lockup} width={150} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default StytchContainer
|
export default StytchContainer;
|
||||||
|
|
|
@ -1,113 +1,113 @@
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
import styles from '../styles/Home.module.css'
|
import styles from "../styles/Home.module.css";
|
||||||
import { sendOTP } from '../lib/otpUtils'
|
import { sendOTP } from "../lib/otpUtils";
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// Handles auto-tabbing to next passcode digit input.
|
// Handles auto-tabbing to next passcode digit input.
|
||||||
// Logic inspired from https://stackoverflow.com/questions/15595652/focus-next-input-once-reaching-maxlength-value.
|
// Logic inspired from https://stackoverflow.com/questions/15595652/focus-next-input-once-reaching-maxlength-value.
|
||||||
const autoTab = (target: HTMLInputElement, key?: string) => {
|
const autoTab = (target: HTMLInputElement, key?: string) => {
|
||||||
if (target.value.length >= target.maxLength) {
|
if (target.value.length >= target.maxLength) {
|
||||||
let next = target
|
let next = target;
|
||||||
while ((next = next.nextElementSibling as HTMLInputElement)) {
|
while ((next = next.nextElementSibling as HTMLInputElement)) {
|
||||||
if (next == null) break
|
if (next == null) break;
|
||||||
if (next.tagName.toLowerCase() === 'input') {
|
if (next.tagName.toLowerCase() === "input") {
|
||||||
next?.focus()
|
next?.focus();
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Move to previous field if empty (user pressed backspace)
|
// Move to previous field if empty (user pressed backspace)
|
||||||
else if (target.value.length === 0) {
|
else if (target.value.length === 0) {
|
||||||
let previous = target
|
let previous = target;
|
||||||
while ((previous = previous.previousElementSibling as HTMLInputElement)) {
|
while ((previous = previous.previousElementSibling as HTMLInputElement)) {
|
||||||
if (previous == null) break
|
if (previous == null) break;
|
||||||
if (previous.tagName.toLowerCase() === 'input') {
|
if (previous.tagName.toLowerCase() === "input") {
|
||||||
previous.focus()
|
previous.focus();
|
||||||
break
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
methodId: string
|
methodId: string;
|
||||||
phoneNumber: string
|
phoneNumber: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const VerifyOTPForm = (props: Props) => {
|
const VerifyOTPForm = (props: Props) => {
|
||||||
const { methodId, phoneNumber } = props
|
const { methodId, phoneNumber } = props;
|
||||||
const [isDisabled, setIsDisabled] = React.useState(true)
|
const [isDisabled, setIsDisabled] = React.useState(true);
|
||||||
const [currentMethodId, setCurrentMethodId] = React.useState(methodId)
|
const [currentMethodId, setCurrentMethodId] = React.useState(methodId);
|
||||||
const [isError, setIsError] = React.useState(false)
|
const [isError, setIsError] = React.useState(false);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const strippedNumber = phoneNumber.replace(/\D/g, '')
|
const strippedNumber = phoneNumber.replace(/\D/g, "");
|
||||||
const parsedPhoneNumber = `(${strippedNumber.slice(
|
const parsedPhoneNumber = `(${strippedNumber.slice(
|
||||||
0,
|
0,
|
||||||
3
|
3,
|
||||||
)}) ${strippedNumber.slice(3, 6)}-${strippedNumber.slice(6, 10)}`
|
)}) ${strippedNumber.slice(3, 6)}-${strippedNumber.slice(6, 10)}`;
|
||||||
|
|
||||||
const isValidPasscode = () => {
|
const isValidPasscode = () => {
|
||||||
const regex = /^[0-9]$/g
|
const regex = /^[0-9]$/g;
|
||||||
const inputs = document.getElementsByClassName(styles.passcodeInput)
|
const inputs = document.getElementsByClassName(styles.passcodeInput);
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
if (!(inputs[i] as HTMLInputElement).value.match(regex)) {
|
if (!(inputs[i] as HTMLInputElement).value.match(regex)) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
const onPasscodeDigitChange = () => {
|
const onPasscodeDigitChange = () => {
|
||||||
if (isValidPasscode()) {
|
if (isValidPasscode()) {
|
||||||
setIsDisabled(false)
|
setIsDisabled(false);
|
||||||
setIsError(false)
|
setIsError(false);
|
||||||
} else {
|
} else {
|
||||||
setIsDisabled(true)
|
setIsDisabled(true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const resetPasscode = () => {
|
const resetPasscode = () => {
|
||||||
const inputs = document.getElementsByClassName(styles.passcodeInput)
|
const inputs = document.getElementsByClassName(styles.passcodeInput);
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
;(inputs[i] as HTMLInputElement).value = ''
|
(inputs[i] as HTMLInputElement).value = "";
|
||||||
}
|
|
||||||
document.getElementById('digit-0')?.focus()
|
|
||||||
setIsDisabled(true)
|
|
||||||
}
|
}
|
||||||
|
document.getElementById("digit-0")?.focus();
|
||||||
|
setIsDisabled(true);
|
||||||
|
};
|
||||||
|
|
||||||
const resendCode = async () => {
|
const resendCode = async () => {
|
||||||
const methodId = await sendOTP(phoneNumber)
|
const methodId = await sendOTP(phoneNumber);
|
||||||
setCurrentMethodId(methodId)
|
setCurrentMethodId(methodId);
|
||||||
resetPasscode()
|
resetPasscode();
|
||||||
setIsError(false)
|
setIsError(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent) => {
|
const onSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
if (isValidPasscode()) {
|
if (isValidPasscode()) {
|
||||||
let otpInput = ''
|
let otpInput = "";
|
||||||
const inputs = document.getElementsByClassName(styles.passcodeInput)
|
const inputs = document.getElementsByClassName(styles.passcodeInput);
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
otpInput += (inputs[i] as HTMLInputElement).value
|
otpInput += (inputs[i] as HTMLInputElement).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await fetch('/api/authenticate_otp', {
|
const resp = await fetch("/api/authenticate_otp", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify({ otpInput, methodId: currentMethodId }),
|
body: JSON.stringify({ otpInput, methodId: currentMethodId }),
|
||||||
})
|
});
|
||||||
|
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
router.push('/profile')
|
router.push("/profile");
|
||||||
} else {
|
} else {
|
||||||
setIsError(true)
|
setIsError(true);
|
||||||
resetPasscode()
|
resetPasscode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderPasscodeInputs = () => {
|
const renderPasscodeInputs = () => {
|
||||||
const inputs = []
|
const inputs = [];
|
||||||
for (let i = 0; i < 6; i += 1) {
|
for (let i = 0; i < 6; i += 1) {
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<input
|
<input
|
||||||
|
@ -121,23 +121,23 @@ const VerifyOTPForm = (props: Props) => {
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
size={1}
|
size={1}
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>,
|
||||||
)
|
);
|
||||||
}
|
|
||||||
return inputs
|
|
||||||
}
|
}
|
||||||
|
return inputs;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Enter passcode</h2>
|
<h2>Enter passcode</h2>
|
||||||
<p className={styles.smsInstructions}>
|
<p className={styles.smsInstructions}>
|
||||||
A 6-digit passcode was sent to you at{' '}
|
A 6-digit passcode was sent to you at{" "}
|
||||||
<strong>{parsedPhoneNumber}</strong>.
|
<strong>{parsedPhoneNumber}</strong>.
|
||||||
</p>
|
</p>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className={styles.passcodeContainer}>
|
<div className={styles.passcodeContainer}>
|
||||||
<p className={styles.errorText}>
|
<p className={styles.errorText}>
|
||||||
{isError ? 'Invalid code. Please try again.' : ''}
|
{isError ? "Invalid code. Please try again." : ""}
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.passcodeInputContainer}>
|
<div className={styles.passcodeInputContainer}>
|
||||||
{renderPasscodeInputs()}
|
{renderPasscodeInputs()}
|
||||||
|
@ -162,7 +162,7 @@ const VerifyOTPForm = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default VerifyOTPForm
|
export default VerifyOTPForm;
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import * as stytch from 'stytch'
|
import * as stytch from "stytch";
|
||||||
|
|
||||||
let client: stytch.Client
|
let client: stytch.Client;
|
||||||
const loadStytch = () => {
|
const loadStytch = () => {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
client = new stytch.Client({
|
client = new stytch.Client({
|
||||||
project_id: process.env.STYTCH_PROJECT_ID || '',
|
project_id: process.env.STYTCH_PROJECT_ID || "",
|
||||||
secret: process.env.STYTCH_SECRET || '',
|
secret: process.env.STYTCH_SECRET || "",
|
||||||
env:
|
env:
|
||||||
process.env.STYTCH_PROJECT_ENV === 'live'
|
process.env.STYTCH_PROJECT_ENV === "live"
|
||||||
? stytch.envs.live
|
? stytch.envs.live
|
||||||
: stytch.envs.test,
|
: stytch.envs.test,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return client
|
return client;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default loadStytch
|
export default loadStytch;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
export async function sendOTP(phoneNumber: string) {
|
export async function sendOTP(phoneNumber: string) {
|
||||||
const resp = await fetch('/api/send_otp', {
|
const resp = await fetch("/api/send_otp", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
intlCode: '+1',
|
intlCode: "+1",
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
}),
|
}),
|
||||||
})
|
});
|
||||||
const data = await resp.json()
|
const data = await resp.json();
|
||||||
return data.methodId
|
return data.methodId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { Session, withIronSession } from 'next-iron-session'
|
import { Session, withIronSession } from "next-iron-session";
|
||||||
|
|
||||||
type NextIronRequest = NextApiRequest & { session: Session }
|
type NextIronRequest = NextApiRequest & { session: Session };
|
||||||
|
|
||||||
type APIHandler = (
|
type APIHandler = (
|
||||||
req: NextIronRequest,
|
req: NextIronRequest,
|
||||||
res: NextApiResponse<any>
|
res: NextApiResponse<any>,
|
||||||
) => Promise<any>
|
) => Promise<any>;
|
||||||
|
|
||||||
export type ServerSideProps = ({
|
export type ServerSideProps = ({
|
||||||
req,
|
req,
|
||||||
}: {
|
}: {
|
||||||
req: NextIronRequest
|
req: NextIronRequest;
|
||||||
}) => Promise<any>
|
}) => Promise<any>;
|
||||||
|
|
||||||
const withSession = (handler: APIHandler | ServerSideProps) =>
|
const withSession = (handler: APIHandler | ServerSideProps) =>
|
||||||
withIronSession(handler, {
|
withIronSession(handler, {
|
||||||
password: process.env.IRON_SESSION_PASSWORD || '',
|
password: process.env.IRON_SESSION_PASSWORD || "",
|
||||||
cookieName: process.env.IRON_SESSION_COOKIE_NAME || '',
|
cookieName: process.env.IRON_SESSION_COOKIE_NAME || "",
|
||||||
// if your localhost is served on http:// then disable the secure flag
|
// if your localhost is served on http:// then disable the secure flag
|
||||||
cookieOptions: {
|
cookieOptions: {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === "production",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export default withSession
|
export default withSession;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import '../styles/globals.css'
|
import "../styles/globals.css";
|
||||||
import styles from '../styles/Home.module.css'
|
import styles from "../styles/Home.module.css";
|
||||||
import type { AppProps } from 'next/app'
|
import type { AppProps } from "next/app";
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
import Head from 'next/head'
|
import Head from "next/head";
|
||||||
import Image from 'next/image'
|
import Image from "next/image";
|
||||||
import stytchLogo from '/public/stytch-logo.svg'
|
import stytchLogo from "/public/stytch-logo.svg";
|
||||||
import vercelLogo from '/public/vercel-logotype-dark.svg'
|
import vercelLogo from "/public/vercel-logotype-dark.svg";
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -50,6 +50,6 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
export default MyApp
|
export default MyApp;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Html, Head, Main, NextScript } from 'next/document'
|
import { Html, Head, Main, NextScript } from "next/document";
|
||||||
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
|
@ -14,5 +14,5 @@ export default function Document() {
|
||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</Html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,39 @@
|
||||||
// This API route authenticates a Stytch magic link.
|
// This API route authenticates a Stytch magic link.
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { Session } from 'next-iron-session'
|
import { Session } from "next-iron-session";
|
||||||
import withSession from '../../lib/withSession'
|
import withSession from "../../lib/withSession";
|
||||||
import loadStytch from '../../lib/loadStytch'
|
import loadStytch from "../../lib/loadStytch";
|
||||||
type NextIronRequest = NextApiRequest & { session: Session }
|
type NextIronRequest = NextApiRequest & { session: Session };
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
errorString: string
|
errorString: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function handler(
|
export async function handler(
|
||||||
req: NextIronRequest,
|
req: NextIronRequest,
|
||||||
res: NextApiResponse<Data>
|
res: NextApiResponse<Data>,
|
||||||
) {
|
) {
|
||||||
if (req.method === 'GET') {
|
if (req.method === "GET") {
|
||||||
const client = loadStytch()
|
const client = loadStytch();
|
||||||
const { token } = req.query
|
const { token } = req.query;
|
||||||
try {
|
try {
|
||||||
const resp = await client.magicLinks.authenticate(token as string)
|
const resp = await client.magicLinks.authenticate(token as string);
|
||||||
// Set session
|
// Set session
|
||||||
req.session.destroy()
|
req.session.destroy();
|
||||||
req.session.set('user', {
|
req.session.set("user", {
|
||||||
id: resp.user_id,
|
id: resp.user_id,
|
||||||
})
|
});
|
||||||
// Save additional user data here
|
// Save additional user data here
|
||||||
await req.session.save()
|
await req.session.save();
|
||||||
res.redirect('/profile')
|
res.redirect("/profile");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorString = JSON.stringify(error)
|
const errorString = JSON.stringify(error);
|
||||||
console.log(error)
|
console.log(error);
|
||||||
res.status(400).json({ errorString })
|
res.status(400).json({ errorString });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle any other HTTP method
|
// Handle any other HTTP method
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSession(handler)
|
export default withSession(handler);
|
||||||
|
|
|
@ -1,51 +1,51 @@
|
||||||
// This API route authenticates Stytch OTP codes.
|
// This API route authenticates Stytch OTP codes.
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { Session } from 'next-iron-session'
|
import { Session } from "next-iron-session";
|
||||||
import withSession from '../../lib/withSession'
|
import withSession from "../../lib/withSession";
|
||||||
import loadStytch from '../../lib/loadStytch'
|
import loadStytch from "../../lib/loadStytch";
|
||||||
type NextIronRequest = NextApiRequest & { session: Session }
|
type NextIronRequest = NextApiRequest & { session: Session };
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
msg: string
|
msg: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function handler(
|
export async function handler(
|
||||||
req: NextIronRequest,
|
req: NextIronRequest,
|
||||||
res: NextApiResponse<Data>
|
res: NextApiResponse<Data>,
|
||||||
) {
|
) {
|
||||||
if (req.method === 'POST') {
|
if (req.method === "POST") {
|
||||||
const client = loadStytch()
|
const client = loadStytch();
|
||||||
const data = JSON.parse(req.body)
|
const data = JSON.parse(req.body);
|
||||||
try {
|
try {
|
||||||
// params are of type stytch.LoginOrCreateUserBySMSRequest
|
// params are of type stytch.LoginOrCreateUserBySMSRequest
|
||||||
const params = {
|
const params = {
|
||||||
code: data.otpInput,
|
code: data.otpInput,
|
||||||
method_id: data.methodId,
|
method_id: data.methodId,
|
||||||
}
|
};
|
||||||
|
|
||||||
const resp = await client.otps.authenticate(params)
|
const resp = await client.otps.authenticate(params);
|
||||||
if (resp.status_code.toString() === '200') {
|
if (resp.status_code.toString() === "200") {
|
||||||
// Set session
|
// Set session
|
||||||
req.session.destroy()
|
req.session.destroy();
|
||||||
// Save additional user data here
|
// Save additional user data here
|
||||||
req.session.set('user', {
|
req.session.set("user", {
|
||||||
id: resp.user_id,
|
id: resp.user_id,
|
||||||
})
|
});
|
||||||
await req.session.save()
|
await req.session.save();
|
||||||
res
|
res
|
||||||
.status(200)
|
.status(200)
|
||||||
.send({ msg: `successfully authenticated ${resp.user_id}` })
|
.send({ msg: `successfully authenticated ${resp.user_id}` });
|
||||||
} else {
|
} else {
|
||||||
throw Error('Error authenticating your code')
|
throw Error("Error authenticating your code");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorString = JSON.stringify(error)
|
const errorString = JSON.stringify(error);
|
||||||
console.log(error)
|
console.log(error);
|
||||||
res.status(400).json({ msg: errorString })
|
res.status(400).json({ msg: errorString });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle any other HTTP method
|
// Handle any other HTTP method
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSession(handler)
|
export default withSession(handler);
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
// This API route logs a user out.
|
// This API route logs a user out.
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { Session } from 'next-iron-session'
|
import { Session } from "next-iron-session";
|
||||||
import withSession from '../../lib/withSession'
|
import withSession from "../../lib/withSession";
|
||||||
type NextIronRequest = NextApiRequest & { session: Session }
|
type NextIronRequest = NextApiRequest & { session: Session };
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
errorString: string
|
errorString: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function handler(
|
export async function handler(
|
||||||
req: NextIronRequest,
|
req: NextIronRequest,
|
||||||
res: NextApiResponse<Data>
|
res: NextApiResponse<Data>,
|
||||||
) {
|
) {
|
||||||
if (req.method === 'POST') {
|
if (req.method === "POST") {
|
||||||
try {
|
try {
|
||||||
// Set session
|
// Set session
|
||||||
req.session.destroy()
|
req.session.destroy();
|
||||||
res.redirect('/')
|
res.redirect("/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorString = JSON.stringify(error)
|
const errorString = JSON.stringify(error);
|
||||||
console.log(error)
|
console.log(error);
|
||||||
res.status(400).json({ errorString })
|
res.status(400).json({ errorString });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle any other HTTP method
|
// Handle any other HTTP method
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSession(handler)
|
export default withSession(handler);
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
// This API route sends an OTP code to a specified number.
|
// This API route sends an OTP code to a specified number.
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import loadStytch from '../../lib/loadStytch'
|
import loadStytch from "../../lib/loadStytch";
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
methodId: string
|
methodId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
||||||
if (req.method === 'POST') {
|
if (req.method === "POST") {
|
||||||
const client = loadStytch()
|
const client = loadStytch();
|
||||||
const data = JSON.parse(req.body)
|
const data = JSON.parse(req.body);
|
||||||
try {
|
try {
|
||||||
const phoneNumber = data.phoneNumber.replace(/\D/g, '')
|
const phoneNumber = data.phoneNumber.replace(/\D/g, "");
|
||||||
|
|
||||||
// params are of type stytch.LoginOrCreateUserBySMSRequest
|
// params are of type stytch.LoginOrCreateUserBySMSRequest
|
||||||
const params = {
|
const params = {
|
||||||
phone_number: `${data.intlCode}${phoneNumber}`,
|
phone_number: `${data.intlCode}${phoneNumber}`,
|
||||||
}
|
};
|
||||||
|
|
||||||
const resp = await client.otps.sms.loginOrCreate(params)
|
const resp = await client.otps.sms.loginOrCreate(params);
|
||||||
res.status(200).json({ methodId: resp.phone_id })
|
res.status(200).json({ methodId: resp.phone_id });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error);
|
||||||
res.status(400)
|
res.status(400);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle any other HTTP method
|
// Handle any other HTTP method
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default handler
|
export default handler;
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from "react";
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
import { Stytch, StytchProps } from '@stytch/stytch-react'
|
import { Stytch, StytchProps } from "@stytch/stytch-react";
|
||||||
import { OAuthProvidersTypes, SDKProductTypes } from '@stytch/stytch-js'
|
import { OAuthProvidersTypes, SDKProductTypes } from "@stytch/stytch-js";
|
||||||
import styles from '../styles/Home.module.css'
|
import styles from "../styles/Home.module.css";
|
||||||
import withSession, { ServerSideProps } from '../lib/withSession'
|
import withSession, { ServerSideProps } from "../lib/withSession";
|
||||||
import LoginWithSMS from '../components/LoginWithSMS'
|
import LoginWithSMS from "../components/LoginWithSMS";
|
||||||
import { LoginMethod } from '../lib/types'
|
import { LoginMethod } from "../lib/types";
|
||||||
import LoginEntryPoint from '../components/LoginEntryPoint'
|
import LoginEntryPoint from "../components/LoginEntryPoint";
|
||||||
|
|
||||||
// Set the URL base for redirect URLs. The three cases are as follows:
|
// Set the URL base for redirect URLs. The three cases are as follows:
|
||||||
// 1. Running locally via `vercel dev`; VERCEL_URL will contain localhost, but will not be https.
|
// 1. Running locally via `vercel dev`; VERCEL_URL will contain localhost, but will not be https.
|
||||||
// 2. Deploying via Vercel; VERCEL_URL will be generated on runtime and use https.
|
// 2. Deploying via Vercel; VERCEL_URL will be generated on runtime and use https.
|
||||||
// 3. Running locally via `npm run dev`; VERCEL_URL will be undefined and the app will be at localhost.
|
// 3. Running locally via `npm run dev`; VERCEL_URL will be undefined and the app will be at localhost.
|
||||||
let REDIRECT_URL_BASE = ''
|
let REDIRECT_URL_BASE = "";
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_VERCEL_URL?.includes('localhost')) {
|
if (process.env.NEXT_PUBLIC_VERCEL_URL?.includes("localhost")) {
|
||||||
REDIRECT_URL_BASE = 'http://localhost:3000'
|
REDIRECT_URL_BASE = "http://localhost:3000";
|
||||||
} else if (process.env.NEXT_PUBLIC_VERCEL_URL !== undefined) {
|
} else if (process.env.NEXT_PUBLIC_VERCEL_URL !== undefined) {
|
||||||
REDIRECT_URL_BASE = `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
|
REDIRECT_URL_BASE = `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
|
||||||
} else {
|
} else {
|
||||||
REDIRECT_URL_BASE = 'http://localhost:3000'
|
REDIRECT_URL_BASE = "http://localhost:3000";
|
||||||
}
|
}
|
||||||
|
|
||||||
const stytchProps: StytchProps = {
|
const stytchProps: StytchProps = {
|
||||||
loginOrSignupView: {
|
loginOrSignupView: {
|
||||||
products: [SDKProductTypes.oauth, SDKProductTypes.emailMagicLinks],
|
products: [SDKProductTypes.oauth, SDKProductTypes.emailMagicLinks],
|
||||||
emailMagicLinksOptions: {
|
emailMagicLinksOptions: {
|
||||||
loginRedirectURL: REDIRECT_URL_BASE + '/api/authenticate_magic_link',
|
loginRedirectURL: REDIRECT_URL_BASE + "/api/authenticate_magic_link",
|
||||||
loginExpirationMinutes: 30,
|
loginExpirationMinutes: 30,
|
||||||
signupRedirectURL: REDIRECT_URL_BASE + '/api/authenticate_magic_link',
|
signupRedirectURL: REDIRECT_URL_BASE + "/api/authenticate_magic_link",
|
||||||
signupExpirationMinutes: 30,
|
signupExpirationMinutes: 30,
|
||||||
createUserAsPending: false,
|
createUserAsPending: false,
|
||||||
},
|
},
|
||||||
|
@ -42,11 +42,11 @@ const stytchProps: StytchProps = {
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
fontFamily: '"Helvetica New", Helvetica, sans-serif',
|
fontFamily: '"Helvetica New", Helvetica, sans-serif',
|
||||||
primaryColor: '#0577CA',
|
primaryColor: "#0577CA",
|
||||||
primaryTextColor: '#090909',
|
primaryTextColor: "#090909",
|
||||||
width: '321px',
|
width: "321px",
|
||||||
},
|
},
|
||||||
publicToken: process.env.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN || '',
|
publicToken: process.env.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN || "",
|
||||||
callbacks: {
|
callbacks: {
|
||||||
onEvent: (data: { eventData: { userId: any; email: any } }) => {
|
onEvent: (data: { eventData: { userId: any; email: any } }) => {
|
||||||
if (
|
if (
|
||||||
|
@ -56,47 +56,49 @@ const stytchProps: StytchProps = {
|
||||||
console.log({
|
console.log({
|
||||||
userId: data.eventData.userId,
|
userId: data.eventData.userId,
|
||||||
email: data.eventData.email,
|
email: data.eventData.email,
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('The user is not found. Data: ', data)
|
console.warn("The user is not found. Data: ", data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: (data) => console.log(data),
|
onSuccess: (data) => console.log(data),
|
||||||
onError: (data) => console.log(data),
|
onError: (data) => console.log(data),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
publicToken: string
|
publicToken: string;
|
||||||
user: {
|
user: {
|
||||||
id: string
|
id: string;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const App = (props: Props) => {
|
const App = (props: Props) => {
|
||||||
const { user, publicToken } = props
|
const { user, publicToken } = props;
|
||||||
const [loginMethod, setLoginMethod] = React.useState<LoginMethod | null>(null)
|
const [loginMethod, setLoginMethod] = React.useState<LoginMethod | null>(
|
||||||
const router = useRouter()
|
null,
|
||||||
|
);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
router.push('/profile')
|
router.push("/profile");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const loginMethodMap: Record<LoginMethod, React.ReactElement> = {
|
const loginMethodMap: Record<LoginMethod, React.ReactElement> = {
|
||||||
[LoginMethod.API]: <LoginWithSMS />,
|
[LoginMethod.API]: <LoginWithSMS />,
|
||||||
[LoginMethod.SDK]: (
|
[LoginMethod.SDK]: (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Stytch
|
<Stytch
|
||||||
publicToken={publicToken || ''}
|
publicToken={publicToken || ""}
|
||||||
loginOrSignupView={stytchProps.loginOrSignupView}
|
loginOrSignupView={stytchProps.loginOrSignupView}
|
||||||
style={stytchProps.style}
|
style={stytchProps.style}
|
||||||
callbacks={stytchProps.callbacks}
|
callbacks={stytchProps.callbacks}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
|
@ -106,19 +108,19 @@ const App = (props: Props) => {
|
||||||
loginMethodMap[loginMethod]
|
loginMethodMap[loginMethod]
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getServerSidePropsHandler: ServerSideProps = async ({ req }) => {
|
const getServerSidePropsHandler: ServerSideProps = async ({ req }) => {
|
||||||
// Get the user's session based on the request
|
// Get the user's session based on the request
|
||||||
const user = req.session.get('user') ?? null
|
const user = req.session.get("user") ?? null;
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
publicToken: stytchProps.publicToken,
|
publicToken: stytchProps.publicToken,
|
||||||
user,
|
user,
|
||||||
}
|
};
|
||||||
return { props }
|
return { props };
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getServerSideProps = withSession(getServerSidePropsHandler)
|
export const getServerSideProps = withSession(getServerSidePropsHandler);
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from "react";
|
||||||
import styles from '../styles/Home.module.css'
|
import styles from "../styles/Home.module.css";
|
||||||
import StytchContainer from '../components/StytchContainer'
|
import StytchContainer from "../components/StytchContainer";
|
||||||
import withSession, { ServerSideProps } from '../lib/withSession'
|
import withSession, { ServerSideProps } from "../lib/withSession";
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user?: {
|
user?: {
|
||||||
id: string
|
id: string;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const Profile = (props: Props) => {
|
const Profile = (props: Props) => {
|
||||||
const { user } = props
|
const { user } = props;
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
router.replace('/')
|
router.replace("/");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const signOut = async () => {
|
const signOut = async () => {
|
||||||
const resp = await fetch('/api/logout', { method: 'POST' })
|
const resp = await fetch("/api/logout", { method: "POST" });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
router.push('/')
|
router.push("/");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -33,12 +33,12 @@ const Profile = (props: Props) => {
|
||||||
<div />
|
<div />
|
||||||
) : (
|
) : (
|
||||||
<StytchContainer>
|
<StytchContainer>
|
||||||
<h2>{'Welcome!'}</h2>
|
<h2>{"Welcome!"}</h2>
|
||||||
<p className={styles.profileSubHeader}>
|
<p className={styles.profileSubHeader}>
|
||||||
Thank you for using Stytch! Here’s your user info.
|
Thank you for using Stytch! Here’s your user info.
|
||||||
</p>
|
</p>
|
||||||
<pre className={styles.code}>
|
<pre className={styles.code}>
|
||||||
{JSON.stringify(user, null, 1).replace(' ', '')}
|
{JSON.stringify(user, null, 1).replace(" ", "")}
|
||||||
</pre>
|
</pre>
|
||||||
<button className={styles.primaryButton} onClick={signOut}>
|
<button className={styles.primaryButton} onClick={signOut}>
|
||||||
Sign out
|
Sign out
|
||||||
|
@ -46,16 +46,16 @@ const Profile = (props: Props) => {
|
||||||
</StytchContainer>
|
</StytchContainer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getServerSidePropsHandler: ServerSideProps = async ({ req }) => {
|
const getServerSidePropsHandler: ServerSideProps = async ({ req }) => {
|
||||||
// Get the user's session based on the request
|
// Get the user's session based on the request
|
||||||
const user = req.session.get('user') ?? null
|
const user = req.session.get("user") ?? null;
|
||||||
const props: Props = { user }
|
const props: Props = { user };
|
||||||
return { props }
|
return { props };
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getServerSideProps = withSession(getServerSidePropsHandler)
|
export const getServerSideProps = withSession(getServerSidePropsHandler);
|
||||||
|
|
||||||
export default Profile
|
export default Profile;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue