未验证 提交 a2b2a2a3 编写于 作者: L Luis Fernando Alvarez D 提交者: GitHub

Auth example with api routes (#8118)

上级 71f9288a
......@@ -22,31 +22,12 @@ cd with-cookie-auth
### Run locally
The repository is setup as a [monorepo](https://zeit.co/examples/monorepo/) so you can run start the development server with `now dev` inside the project folder.
Install the packages of `/api` and `/www` using `npm` or `yarn`:
```bash
cd api
npm install
cd ../www
npm install
```
Now you can start the development server in the root folder:
```bash
now dev
```
You can configure the `API_URL` environment variable (defaults to `http://localhost:3000`) with [Now env](https://zeit.co/docs/v2/development/environment-variables/#using-now.json) in the `now.json` file:
After you clone the repository you can install the dependencies, run `yarn dev` and start hacking! You'll be able to see the application running locally as if it were deployed.
```bash
"build": {
"env": {
"API_URL": "https://example.com"
}
},
$ cd with-cookie-auth
$ (with-cookie-auth/) yarn install
$ (with-cookie-auth/) yarn dev
```
### Deploy
......@@ -63,8 +44,8 @@ In this example, we authenticate users and store a token in a cookie. The exampl
This example is backend agnostic and uses [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch) to do the API calls on the client and the server.
The repo includes a minimal passwordless backend built with [Micro](https://www.npmjs.com/package/micro) that logs the user in with a GitHub username and saves the user id from the API call as token.
The repo includes a minimal passwordless backend built with the new [API Routes support](https://github.com/zeit/next.js/pull/7296) (`pages/api`), [Micro](https://www.npmjs.com/package/micro) and the [GitHub API](https://developer.github.com/v3/). The backend allows the user to log in with their GitHub username.
Session is synchronized across tabs. If you logout your session gets logged out on all the windows as well. We use the HOC `withAuthSync` for this.
Session is synchronized across tabs. If you logout your session gets removed on all the windows as well. We use the HOC `withAuthSync` for this.
The helper function `auth` helps to retrieve the token across pages and redirects the user if not token was found.
const { json, send, createError, run } = require('micro')
const fetch = require('isomorphic-unfetch')
const login = async (req, res) => {
const { username } = await json(req)
const url = `https://api.github.com/users/${username}`
try {
const response = await fetch(url)
if (response.ok) {
const { id } = await response.json()
send(res, 200, { token: id })
} else {
send(res, response.status, response.statusText)
}
} catch (error) {
throw createError(error.statusCode, error.statusText)
}
}
module.exports = (req, res) => run(req, res, login)
{
"name": "api",
"version": "1.0.0",
"scripts": {
"start": "micro"
},
"license": "ISC",
"dependencies": {
"isomorphic-unfetch": "^3.0.0",
"micro": "^9.3.3"
}
}
module.exports = {
target: 'serverless'
}
{
"version": 2,
"name": "with-cookie-auth",
"build": {
"env": {
"API_URL": "http://localhost:3000"
}
},
"builds": [
{ "src": "www/package.json", "use": "@now/next" },
{ "src": "api/*.js", "use": "@now/node" }
],
"routes": [
{ "src": "/api/(.*)", "dest": "/api/$1" },
{ "src": "/(.*)", "dest": "/www/$1" }
]
}
{
"name": "with-cookie-auth",
"version": "1.0.0",
"description": "Monorepo with a Next.js example of cookie based authorization"
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"isomorphic-unfetch": "^3.0.0",
"js-cookie": "^2.2.0",
"next": "^9.0.1",
"next-cookies": "^1.0.4",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}
import fetch from 'isomorphic-unfetch'
export default async (req, res) => {
const { username } = await req.body
console.log('username', username)
const url = `https://api.github.com/users/${username}`
try {
const response = await fetch(url)
if (response.ok) {
const { id } = await response.json()
return res.status(200).json({ token: id })
} else {
// https://github.com/developit/unfetch#caveats
const error = new Error(response.statusText)
error.response = response
throw error
}
} catch (error) {
const { response } = error
return response
? res.status(response.status).json({ message: response.statusText })
: res.status(400).json({ message: error.message })
}
}
const { send, createError, run } = require('micro')
const fetch = require('isomorphic-unfetch')
import fetch from 'isomorphic-unfetch'
const profile = async (req, res) => {
export default async (req, res) => {
if (!('authorization' in req.headers)) {
throw createError(401, 'Authorization header missing')
return res.status(401).send('Authorization header missing')
}
const auth = await req.headers.authorization
const { token } = JSON.parse(auth)
const url = `https://api.github.com/user/${token}`
try {
const { token } = JSON.parse(auth)
const url = `https://api.github.com/user/${token}`
const response = await fetch(url)
if (response.ok) {
const js = await response.json()
// Need camelcase in the frontend
const data = Object.assign({}, { avatarUrl: js.avatar_url }, js)
send(res, 200, { data })
return res.status(200).json({ data })
} else {
send(res, response.status, response.statusText)
// https://github.com/developit/unfetch#caveats
const error = new Error(response.statusText)
error.response = response
throw error
}
} catch (error) {
throw createError(error.statusCode, error.statusText)
const { response } = error
return response
? res.status(response.status).json({ message: response.statusText })
: res.status(400).json({ message: error.message })
}
}
module.exports = (req, res) => run(req, res, profile)
import React from 'react'
import Layout from '../components/layout'
const Home = props => (
const Home = () => (
<Layout>
<h1>Cookie-based authentication example</h1>
......
import React, { useState } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/layout'
import { login } from '../utils/auth'
function Login () {
const [userData, setUserData] = useState({ username: '', error: '' })
async function handleSubmit (event) {
event.preventDefault()
setUserData(Object.assign({}, userData, { error: '' }))
const username = userData.username
const url = '/api/login'
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
if (response.status === 200) {
const { token } = await response.json()
await login({ token })
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
let error = new Error(response.statusText)
error.response = response
throw error
}
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
const { response } = error
setUserData(
Object.assign({}, userData, {
error: response ? response.statusText : error.message
})
)
}
}
return (
<Layout>
<div className='login'>
<form onSubmit={handleSubmit}>
<label htmlFor='username'>GitHub username</label>
<input
type='text'
id='username'
name='username'
value={userData.username}
onChange={event =>
setUserData(
Object.assign({}, userData, { username: event.target.value })
)
}
/>
<button type='submit'>Login</button>
{userData.error && <p className='error'>Error: {userData.error}</p>}
</form>
</div>
<style jsx>{`
.login {
max-width: 340px;
margin: 0 auto;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
form {
display: flex;
flex-flow: column;
}
label {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
margin: 0.5rem 0 0;
color: brown;
}
`}</style>
</Layout>
)
}
export default Login
import React from 'react'
import Router from 'next/router'
import fetch from 'isomorphic-unfetch'
import nextCookie from 'next-cookies'
import Layout from '../components/layout'
import { withAuthSync } from '../utils/auth'
import getHost from '../utils/get-host'
const Profile = props => {
const { name, login, bio, avatarUrl } = props.data
......@@ -41,7 +43,7 @@ const Profile = props => {
Profile.getInitialProps = async ctx => {
const { token } = nextCookie(ctx)
const url = `${process.env.API_URL}/api/profile.js`
const apiUrl = getHost(ctx.req) + '/api/profile'
const redirectOnError = () =>
typeof window !== 'undefined'
......@@ -49,20 +51,21 @@ Profile.getInitialProps = async ctx => {
: ctx.res.writeHead(302, { Location: '/login' }).end()
try {
const response = await fetch(url, {
const response = await fetch(apiUrl, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authorization: JSON.stringify({ token })
}
})
if (response.ok) {
return await response.json()
const js = await response.json()
console.log('js', js)
return js
} else {
// https://github.com/developit/unfetch#caveats
return await redirectOnError()
}
// https://github.com/developit/unfetch#caveats
return redirectOnError()
} catch (error) {
// Implementation or Network error
return redirectOnError()
......
......@@ -3,12 +3,12 @@ import Router from 'next/router'
import nextCookie from 'next-cookies'
import cookie from 'js-cookie'
export const login = async ({ token }) => {
function login ({ token }) {
cookie.set('token', token, { expires: 1 })
Router.push('/profile')
}
export const logout = () => {
function logout () {
cookie.remove('token')
// to support logging out from all windows
window.localStorage.setItem('logout', Date.now())
......@@ -19,8 +19,8 @@ export const logout = () => {
const getDisplayName = Component =>
Component.displayName || Component.name || 'Component'
export const withAuthSync = WrappedComponent =>
class extends Component {
function withAuthSync (WrappedComponent) {
return class extends Component {
static displayName = `withAuthSync(${getDisplayName(WrappedComponent)})`
static async getInitialProps (ctx) {
......@@ -59,19 +59,18 @@ export const withAuthSync = WrappedComponent =>
return <WrappedComponent {...this.props} />
}
}
}
export const auth = ctx => {
function auth (ctx) {
const { token } = nextCookie(ctx)
/*
* This happens on server only, ctx.req is available means it's being
* rendered on server. If we are on server and token is not available,
* means user is not logged in.
* If `ctx.req` is available it means we are on the server.
* Additionally if there's no token it means the user is not logged in.
*/
if (ctx.req && !token) {
ctx.res.writeHead(302, { Location: '/login' })
ctx.res.end()
return
}
// We already checked for server. This should only happen on client.
......@@ -81,3 +80,5 @@ export const auth = ctx => {
return token
}
export { login, logout, withAuthSync, auth }
// This is not production ready, (except with providers that ensure a secure host, like Now)
// For production consider the usage of environment variables and NODE_ENV
function getHost (req) {
if (!req) return ''
const { host } = req.headers
if (host.startsWith('localhost')) {
return `http://${host}`
}
return `https://${host}`
}
export default getHost
module.exports = {
target: 'serverless',
env: {
API_URL: process.env.API_URL || 'http://localhost:3000'
}
}
{
"name": "with-cookie-auth",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"isomorphic-unfetch": "^3.0.0",
"js-cookie": "^2.2.0",
"next": "latest",
"next-cookies": "^1.0.4",
"react": "^16.7.0",
"react-dom": "^16.7.0"
}
}
import { Component } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/layout'
import { login } from '../utils/auth'
class Login extends Component {
constructor (props) {
super(props)
this.state = { username: '', error: '' }
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange (event) {
this.setState({ username: event.target.value })
}
async handleSubmit (event) {
event.preventDefault()
this.setState({ error: '' })
const username = this.state.username
const url = `${process.env.API_URL}/api/login.js`
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
if (response.ok) {
const { token } = await response.json()
login({ token })
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
let error = new Error(response.statusText)
error.response = response
throw error
}
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
this.setState({ error: error.message })
}
}
render () {
return (
<Layout>
<div className='login'>
<form onSubmit={this.handleSubmit}>
<label htmlFor='username'>GitHub username</label>
<input
type='text'
id='username'
name='username'
value={this.state.username}
onChange={this.handleChange}
/>
<button type='submit'>Login</button>
<p className={`error ${this.state.error && 'show'}`}>
{this.state.error && `Error: ${this.state.error}`}
</p>
</form>
</div>
<style jsx>{`
.login {
max-width: 340px;
margin: 0 auto;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
form {
display: flex;
flex-flow: column;
}
label {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
margin: 0.5rem 0 0;
display: none;
color: brown;
}
.error.show {
display: block;
}
`}</style>
</Layout>
)
}
}
export default Login
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册