custom auth page

This commit is contained in:
keyan 2021-04-24 16:05:07 -05:00
parent ec3f6b922d
commit 900b70da77
14 changed files with 213 additions and 11 deletions

View File

@ -67,6 +67,13 @@ export default {
FROM "Item"
WHERE "parentId" IS NULL`)
},
recent: async (parent, args, { models }) => {
return await models.$queryRaw(`
${SELECT}
FROM "Item"
WHERE "parentId" IS NULL
ORDER BY created_at`)
},
item: async (parent, { id }, { models }) => {
return (await models.$queryRaw(`
${SELECT}
@ -78,7 +85,7 @@ export default {
${SELECT}
FROM "Item"
WHERE "userId" = ${userId} AND "parentId" IS NULL
ORDER BY created_at DESC`)
ORDER BY created_at`)
},
comments: async (parent, { parentId }, { models }) => {
const flat = await models.$queryRaw(`

View File

@ -3,6 +3,7 @@ import { gql } from 'apollo-server-micro'
export default gql`
extend type Query {
items: [Item!]!
recent: [Item!]!
item(id: ID!): Item
userItems(userId: ID!): [Item!]
comments(parentId: ID!): [Item!]!

View File

@ -3,7 +3,7 @@ import InputGroup from 'react-bootstrap/InputGroup'
import BootstrapForm from 'react-bootstrap/Form'
import Alert from 'react-bootstrap/Alert'
import { Formik, Form as FormikForm, useFormikContext, useField } from 'formik'
import { useState } from 'react'
import { useRef, useState } from 'react'
export function SubmitButton ({ children, variant, ...props }) {
const { isSubmitting } = useFormikContext()
@ -63,8 +63,8 @@ export function Form ({
initialValues={initial}
validationSchema={schema}
validateOnBlur={false}
onSubmit={(...args) =>
onSubmit(...args).catch(e => setError(e.message))}
onSubmit={async (...args) =>
onSubmit && onSubmit(...args).catch(e => setError(e.message))}
>
<FormikForm {...props} noValidate>
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
@ -73,3 +73,30 @@ export function Form ({
</Formik>
)
}
export function SyncForm ({
initial, schema, children, action, ...props
}) {
const ref = useRef(null)
return (
<Formik
initialValues={initial}
validationSchema={schema}
validateOnBlur={false}
onSubmit={() => ref.current.submit()}
>
{props => (
<form
ref={ref}
onSubmit={props.handleSubmit}
onReset={props.handleReset}
action={action}
method='POST'
noValidate
>
{children}
</form>
)}
</Formik>
)
}

View File

@ -29,7 +29,7 @@ export default function Header () {
</>
)
} else {
return <Nav.Link onClick={signIn}>login</Nav.Link>
return <Nav.Link href='/login' onClick={signIn}>login</Nav.Link>
}
}
@ -40,7 +40,7 @@ export default function Header () {
<Link href='/' passHref>
<Navbar.Brand className={styles.brand}>STACKER NEWS</Navbar.Brand>
</Link>
<Nav className='mr-auto align-items-center' activeKey={router.asPath}>
<Nav className='mr-auto align-items-center' activeKey={router.asPath.split('?')[0]}>
<Nav.Item>
<Link href='/recent' passHref>
<Nav.Link>recent</Nav.Link>
@ -52,7 +52,7 @@ export default function Header () {
</Link>
</Nav.Item>
</Nav>
<Nav className='ml-auto align-items-center' activeKey={router.asPath}>
<Nav className='ml-auto align-items-center' activeKey={router.asPath.split('?')[0]}>
<Corner />
</Nav>
</Container>

View File

@ -1,6 +1,7 @@
.upvote {
fill: grey;
min-width: fit-content;
user-select: none;
}
.upvote:hover {

View File

@ -22,3 +22,12 @@ export const ITEMS_FEED = gql`
...ItemFields
}
}`
export const ITEMS_RECENT = gql`
${ITEM_FIELDS}
{
items: recent {
...ItemFields
}
}`

View File

@ -1,4 +1,3 @@
import { NextApiHandler } from 'next'
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
import Adapters from 'next-auth/adapters'
@ -10,7 +9,11 @@ const options = {
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
clientSecret: process.env.GITHUB_SECRET
}),
Providers.Twitter({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET
}),
Providers.Email({
server: process.env.EMAIL_SERVER,
@ -19,4 +22,7 @@ const options = {
],
adapter: Adapters.Prisma.Adapter({ prisma }),
secret: process.env.SECRET,
}
pages: {
signIn: '/login'
}
}

102
pages/login.js Normal file
View File

@ -0,0 +1,102 @@
import { providers, signIn, getSession, csrfToken } from 'next-auth/client'
import Layout from '../components/layout'
import Button from 'react-bootstrap/Button'
import styles from '../styles/login.module.css'
import GithubIcon from '../svgs/github-fill.svg'
import TwitterIcon from '../svgs/twitter-fill.svg'
import { Input, SubmitButton, SyncForm } from '../components/form'
import * as Yup from 'yup'
import { useState } from 'react'
import Alert from 'react-bootstrap/Alert'
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
const session = await getSession({ req })
if (session && res && session.accessToken) {
res.writeHead(302, {
Location: callbackUrl
})
res.end()
return
}
return {
props: {
providers: await providers({ req, res }),
csrfToken: await csrfToken({ req, res }),
error
}
}
}
export const EmailSchema = Yup.object({
email: Yup.string().email('email is no good').required('required').trim()
})
export default function login ({ providers, csrfToken, error }) {
const errors = {
Signin: 'Try signing with a different account.',
OAuthSignin: 'Try signing with a different account.',
OAuthCallback: 'Try signing with a different account.',
OAuthCreateAccount: 'Try signing with a different account.',
EmailCreateAccount: 'Try signing with a different account.',
Callback: 'Try signing with a different account.',
OAuthAccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.',
EmailSignin: 'Check your email address.',
CredentialsSignin: 'Sign in failed. Check the details you provided are correct.',
default: 'Unable to sign in.'
}
const [errorMessage, setErrorMessage] = useState(error && (errors[error] ?? errors.default))
return (
<Layout noContain>
<div className={styles.page}>
<div className={styles.login}>
{errorMessage &&
<Alert variant='danger' onClose={() => setErrorMessage(undefined)} dismissible>{errorMessage}</Alert>}
{Object.values(providers).map(provider => {
if (provider.name === 'Email') {
return null
}
const [variant, Icon] =
provider.name === 'Twitter'
? ['twitter', TwitterIcon]
: ['dark', GithubIcon]
return (
<Button
className={`d-block mt-2 ${styles.providerButton}`}
key={provider.name}
variant={variant}
onClick={() => signIn(provider.id)}
>
<Icon
className='mr-3'
/>Login with {provider.name}
</Button>
)
})}
<div className='mt-2 text-center text-muted font-weight-bold'>or</div>
<SyncForm
initial={{
email: ''
}}
schema={EmailSchema}
action='/api/auth/signin/email'
>
<input name='csrfToken' type='hidden' defaultValue={csrfToken} />
<Input
label='Email'
name='email'
placeholder='email@example.com'
required
autoFocus
/>
<SubmitButton variant='secondary' className={styles.providerButton}>Login with Email</SubmitButton>
</SyncForm>
</div>
</div>
</Layout>
)
}

11
pages/recent.js Normal file
View File

@ -0,0 +1,11 @@
import Layout from '../components/layout'
import Items from '../components/items'
import { ITEMS_RECENT } from '../fragments/items'
export default function Index () {
return (
<Layout>
<Items query={ITEMS_RECENT} rank />
</Layout>
)
}

View File

@ -3,7 +3,8 @@ $theme-colors: (
"secondary" : #F6911D,
"danger" : #c03221,
"info" : #007cbe,
"success" : #5c8001
"success" : #5c8001,
"twitter" : #1da1f2,
);
$body-bg: #fafafa;
@ -59,6 +60,14 @@ $container-max-widths: (
color: #ffffff;
}
.btn-twitter svg {
fill: #ffffff;
}
.btn-dark svg {
fill: #ffffff;
}
.nav-link.active {
font-weight: bold;
}

25
styles/login.module.css Normal file
View File

@ -0,0 +1,25 @@
.login .providerButton {
width: 100%;
}
.page {
position: absolute;
width: 100%;
height: 100%;
display: table;
margin: 0;
padding: 0;
top: 0;
z-index: -1;
}
.login {
display: table-cell;
vertical-align: middle;
padding: .5rem;
}
.login > * {
max-width: 300px;
margin: auto;
}

View File

@ -5,6 +5,8 @@
display: table;
margin: 0;
padding: 0;
top: 0;
z-index: -1;
}
.post {

1
svgs/github-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 2C6.475 2 2 6.475 2 12a9.994 9.994 0 0 0 6.838 9.488c.5.087.687-.213.687-.476 0-.237-.013-1.024-.013-1.862-2.512.463-3.162-.612-3.362-1.175-.113-.288-.6-1.175-1.025-1.413-.35-.187-.85-.65-.013-.662.788-.013 1.35.725 1.538 1.025.9 1.512 2.338 1.087 2.912.825.088-.65.35-1.087.638-1.337-2.225-.25-4.55-1.113-4.55-4.938 0-1.088.387-1.987 1.025-2.688-.1-.25-.45-1.275.1-2.65 0 0 .837-.262 2.75 1.026a9.28 9.28 0 0 1 2.5-.338c.85 0 1.7.112 2.5.337 1.912-1.3 2.75-1.024 2.75-1.024.55 1.375.2 2.4.1 2.65.637.7 1.025 1.587 1.025 2.687 0 3.838-2.337 4.688-4.562 4.938.362.312.675.912.675 1.85 0 1.337-.013 2.412-.013 2.75 0 .262.188.574.688.474A10.016 10.016 0 0 0 22 12c0-5.525-4.475-10-10-10z"/></svg>

After

Width:  |  Height:  |  Size: 827 B

1
svgs/twitter-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20"><path fill="none" d="M0 0h24v24H0z"/><path d="M22.162 5.656a8.384 8.384 0 0 1-2.402.658A4.196 4.196 0 0 0 21.6 4c-.82.488-1.719.83-2.656 1.015a4.182 4.182 0 0 0-7.126 3.814 11.874 11.874 0 0 1-8.62-4.37 4.168 4.168 0 0 0-.566 2.103c0 1.45.738 2.731 1.86 3.481a4.168 4.168 0 0 1-1.894-.523v.052a4.185 4.185 0 0 0 3.355 4.101 4.21 4.21 0 0 1-1.89.072A4.185 4.185 0 0 0 7.97 16.65a8.394 8.394 0 0 1-6.191 1.732 11.83 11.83 0 0 0 6.41 1.88c7.693 0 11.9-6.373 11.9-11.9 0-.18-.005-.362-.013-.54a8.496 8.496 0 0 0 2.087-2.165z"/></svg>

After

Width:  |  Height:  |  Size: 612 B