custom auth page
This commit is contained in:
parent
ec3f6b922d
commit
900b70da77
@ -67,6 +67,13 @@ export default {
|
|||||||
FROM "Item"
|
FROM "Item"
|
||||||
WHERE "parentId" IS NULL`)
|
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 }) => {
|
item: async (parent, { id }, { models }) => {
|
||||||
return (await models.$queryRaw(`
|
return (await models.$queryRaw(`
|
||||||
${SELECT}
|
${SELECT}
|
||||||
@ -78,7 +85,7 @@ export default {
|
|||||||
${SELECT}
|
${SELECT}
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
WHERE "userId" = ${userId} AND "parentId" IS NULL
|
WHERE "userId" = ${userId} AND "parentId" IS NULL
|
||||||
ORDER BY created_at DESC`)
|
ORDER BY created_at`)
|
||||||
},
|
},
|
||||||
comments: async (parent, { parentId }, { models }) => {
|
comments: async (parent, { parentId }, { models }) => {
|
||||||
const flat = await models.$queryRaw(`
|
const flat = await models.$queryRaw(`
|
||||||
|
@ -3,6 +3,7 @@ import { gql } from 'apollo-server-micro'
|
|||||||
export default gql`
|
export default gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
items: [Item!]!
|
items: [Item!]!
|
||||||
|
recent: [Item!]!
|
||||||
item(id: ID!): Item
|
item(id: ID!): Item
|
||||||
userItems(userId: ID!): [Item!]
|
userItems(userId: ID!): [Item!]
|
||||||
comments(parentId: ID!): [Item!]!
|
comments(parentId: ID!): [Item!]!
|
||||||
|
@ -3,7 +3,7 @@ import InputGroup from 'react-bootstrap/InputGroup'
|
|||||||
import BootstrapForm from 'react-bootstrap/Form'
|
import BootstrapForm from 'react-bootstrap/Form'
|
||||||
import Alert from 'react-bootstrap/Alert'
|
import Alert from 'react-bootstrap/Alert'
|
||||||
import { Formik, Form as FormikForm, useFormikContext, useField } from 'formik'
|
import { Formik, Form as FormikForm, useFormikContext, useField } from 'formik'
|
||||||
import { useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
export function SubmitButton ({ children, variant, ...props }) {
|
export function SubmitButton ({ children, variant, ...props }) {
|
||||||
const { isSubmitting } = useFormikContext()
|
const { isSubmitting } = useFormikContext()
|
||||||
@ -63,8 +63,8 @@ export function Form ({
|
|||||||
initialValues={initial}
|
initialValues={initial}
|
||||||
validationSchema={schema}
|
validationSchema={schema}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
onSubmit={(...args) =>
|
onSubmit={async (...args) =>
|
||||||
onSubmit(...args).catch(e => setError(e.message))}
|
onSubmit && onSubmit(...args).catch(e => setError(e.message))}
|
||||||
>
|
>
|
||||||
<FormikForm {...props} noValidate>
|
<FormikForm {...props} noValidate>
|
||||||
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
|
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
|
||||||
@ -73,3 +73,30 @@ export function Form ({
|
|||||||
</Formik>
|
</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 {
|
} 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>
|
<Link href='/' passHref>
|
||||||
<Navbar.Brand className={styles.brand}>STACKER NEWS</Navbar.Brand>
|
<Navbar.Brand className={styles.brand}>STACKER NEWS</Navbar.Brand>
|
||||||
</Link>
|
</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>
|
<Nav.Item>
|
||||||
<Link href='/recent' passHref>
|
<Link href='/recent' passHref>
|
||||||
<Nav.Link>recent</Nav.Link>
|
<Nav.Link>recent</Nav.Link>
|
||||||
@ -52,7 +52,7 @@ export default function Header () {
|
|||||||
</Link>
|
</Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
</Nav>
|
</Nav>
|
||||||
<Nav className='ml-auto align-items-center' activeKey={router.asPath}>
|
<Nav className='ml-auto align-items-center' activeKey={router.asPath.split('?')[0]}>
|
||||||
<Corner />
|
<Corner />
|
||||||
</Nav>
|
</Nav>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
.upvote {
|
.upvote {
|
||||||
fill: grey;
|
fill: grey;
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upvote:hover {
|
.upvote:hover {
|
||||||
|
@ -22,3 +22,12 @@ export const ITEMS_FEED = gql`
|
|||||||
...ItemFields
|
...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 NextAuth from 'next-auth'
|
||||||
import Providers from 'next-auth/providers'
|
import Providers from 'next-auth/providers'
|
||||||
import Adapters from 'next-auth/adapters'
|
import Adapters from 'next-auth/adapters'
|
||||||
@ -10,7 +9,11 @@ const options = {
|
|||||||
providers: [
|
providers: [
|
||||||
Providers.GitHub({
|
Providers.GitHub({
|
||||||
clientId: process.env.GITHUB_ID,
|
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({
|
Providers.Email({
|
||||||
server: process.env.EMAIL_SERVER,
|
server: process.env.EMAIL_SERVER,
|
||||||
@ -19,4 +22,7 @@ const options = {
|
|||||||
],
|
],
|
||||||
adapter: Adapters.Prisma.Adapter({ prisma }),
|
adapter: Adapters.Prisma.Adapter({ prisma }),
|
||||||
secret: process.env.SECRET,
|
secret: process.env.SECRET,
|
||||||
}
|
pages: {
|
||||||
|
signIn: '/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
102
pages/login.js
Normal file
102
pages/login.js
Normal 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
11
pages/recent.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -3,7 +3,8 @@ $theme-colors: (
|
|||||||
"secondary" : #F6911D,
|
"secondary" : #F6911D,
|
||||||
"danger" : #c03221,
|
"danger" : #c03221,
|
||||||
"info" : #007cbe,
|
"info" : #007cbe,
|
||||||
"success" : #5c8001
|
"success" : #5c8001,
|
||||||
|
"twitter" : #1da1f2,
|
||||||
);
|
);
|
||||||
|
|
||||||
$body-bg: #fafafa;
|
$body-bg: #fafafa;
|
||||||
@ -59,6 +60,14 @@ $container-max-widths: (
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-twitter svg {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dark svg {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-link.active {
|
.nav-link.active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
25
styles/login.module.css
Normal file
25
styles/login.module.css
Normal 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;
|
||||||
|
}
|
@ -5,6 +5,8 @@
|
|||||||
display: table;
|
display: table;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
|
1
svgs/github-fill.svg
Normal file
1
svgs/github-fill.svg
Normal 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
1
svgs/twitter-fill.svg
Normal 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 |
Loading…
x
Reference in New Issue
Block a user