custom auth page
This commit is contained in:
parent
ec3f6b922d
commit
900b70da77
|
@ -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(`
|
||||
|
|
|
@ -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!]!
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.upvote {
|
||||
fill: grey;
|
||||
min-width: fit-content;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.upvote:hover {
|
||||
|
|
|
@ -22,3 +22,12 @@ export const ITEMS_FEED = gql`
|
|||
...ItemFields
|
||||
}
|
||||
}`
|
||||
|
||||
export const ITEMS_RECENT = gql`
|
||||
${ITEM_FIELDS}
|
||||
|
||||
{
|
||||
items: recent {
|
||||
...ItemFields
|
||||
}
|
||||
}`
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
display: table;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.post {
|
||||
|
|
|
@ -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 |
|
@ -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 |
Loading…
Reference in New Issue