forked from acouzens/open5gs
add CSRF for protect Cross Site Attack
This commit is contained in:
parent
ceb1859dba
commit
25f7275677
|
@ -0,0 +1,19 @@
|
|||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import Package from '../package'
|
||||
|
||||
export default() => (
|
||||
<footer>
|
||||
<div className="container">
|
||||
<hr/>
|
||||
<p>
|
||||
<Link prefetch href="/"><a><strong>Home</strong></a></Link>
|
||||
|
|
||||
<Link href="https://github.com/acetcom/cellwire"><a>NextEPC {Package.version}</a></Link>
|
||||
|
|
||||
<Link href="https://github.com/zeit/next.js"><a>nextjs {Package.dependencies.next}</a></Link>
|
||||
| © {new Date().getYear() + 1900}
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
|
@ -0,0 +1,30 @@
|
|||
/* eslint-disable react/no-danger */
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import Menu from './menu'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static propTypes() {
|
||||
return {
|
||||
session: React.PropTypes.object.isRequired
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<header>
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
</Head>
|
||||
<Menu session={this.props.session}/>
|
||||
<div className="header">
|
||||
<h1><Link prefetch href="/"><a>NextEPC Project</a></Link></h1>
|
||||
<hr/>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,32 +1,26 @@
|
|||
import Link from 'next/link'
|
||||
import Head from 'next/head'
|
||||
import React from 'react'
|
||||
import Header from './header'
|
||||
import Footer from './footer'
|
||||
|
||||
const Layout = ({
|
||||
children, title = 'This is the default title',
|
||||
isAuthenticated, currentUrl }) => {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{ title }</title>
|
||||
<meta charSet='utf-8' />
|
||||
<meta name='viewport' content='initial-scale=1.0, width=device-width' />
|
||||
</Head>
|
||||
<header>
|
||||
<nav>
|
||||
<Link prefetch key='/about' href='/about'><a>About</a></Link> |
|
||||
<Link prefetch key='/contact' href='/contact'><a>Contact</a></Link>
|
||||
{isAuthenticated && <p>isAuthenticated</p>}
|
||||
{currentUrl && <p>{currentUrl}</p>}
|
||||
</nav>
|
||||
</header>
|
||||
export default class extends React.Component {
|
||||
|
||||
{ children }
|
||||
static propTypes() {
|
||||
return {
|
||||
session: React.PropTypes.object.isRequired,
|
||||
children: React.PropTypes.object.isRequired
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Header session={this.props.session}/>
|
||||
<div className="container">
|
||||
{ this.props.children }
|
||||
</div>
|
||||
<Footer/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<footer>
|
||||
I'm here to say
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
|
@ -0,0 +1,48 @@
|
|||
/* global window */
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import Session from './session'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static propTypes() {
|
||||
return {
|
||||
session: React.PropTypes.object.isRequired
|
||||
}
|
||||
}
|
||||
|
||||
async handleSubmit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
const session = new Session()
|
||||
await session.signout()
|
||||
|
||||
// @FIXME next/router not working reliably so using window.location
|
||||
window.location = '/'
|
||||
}
|
||||
|
||||
render() {
|
||||
const session = this.props.session || null
|
||||
|
||||
let loginMessage = <p><Link prefetch href="/"><a className="home">Home</a></Link> You are not logged in. <Link prefetch href="/login"><a>Sign in</a></Link></p>
|
||||
|
||||
if (session.user) {
|
||||
loginMessage = (
|
||||
<form id="signout" method="post" action="/logout" onSubmit={this.handleSubmit}>
|
||||
<input name="_csrf" type="hidden" value={session.csrfToken}/>
|
||||
<p>
|
||||
<Link prefetch href="/"><a className="home">Home</a></Link>Logged in as <strong><Link prefetch href="/login"><a>{session.user.username || session.user.role}</a></Link></strong>
|
||||
<button type="submit">Sign out</button>
|
||||
</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="menubar">
|
||||
{loginMessage}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react'
|
||||
import Session from './session'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
// Expose session to all pages
|
||||
static async getInitialProps({req}) {
|
||||
const session = new Session({req})
|
||||
return {session: await session.getSession()}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
/* global window */
|
||||
/* global localStorage */
|
||||
/* global XMLHttpRequest */
|
||||
/**
|
||||
* A class to handle signing in and out and caching session data in sessionStore
|
||||
*
|
||||
* Note: We use XMLHttpRequest() here rather than fetch because fetch() uses
|
||||
* Service Workers and they cannot share cookies with the browser session
|
||||
* yet (!) so if we tried to get or pass the CSRF token it would mismatch.
|
||||
*/
|
||||
|
||||
export default class Session {
|
||||
|
||||
constructor({req} = {}) {
|
||||
this._session = {}
|
||||
try {
|
||||
if (req) {
|
||||
// If running on server we can access the server side environment
|
||||
this._session = {
|
||||
csrfToken: req.connection._httpMessage.locals._csrf
|
||||
}
|
||||
// If the session is associated with a user add user object to session
|
||||
if (req.user) {
|
||||
this._session.user = req.user
|
||||
}
|
||||
} else {
|
||||
// If running on client, attempt to load session from localStorage
|
||||
this._session = this._getLocalStore('session')
|
||||
}
|
||||
} catch (err) {
|
||||
// Handle if error reading from localStorage or server state is safe to
|
||||
// ignore (will just cause session data to be fetched by ajax)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
static async getCsrfToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof window === 'undefined') {
|
||||
return reject(Error('This method should only be called on the client'))
|
||||
}
|
||||
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', '/csrf', true)
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
const responseJson = JSON.parse(xhr.responseText)
|
||||
resolve(responseJson.csrfToken)
|
||||
} else {
|
||||
reject(Error('Unexpected response when trying to get CSRF token'))
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
reject(Error('XMLHttpRequest error: Unable to get CSRF token'))
|
||||
}
|
||||
xhr.send()
|
||||
})
|
||||
}
|
||||
|
||||
// We can't do async requests in the constructor so access is via asyc method
|
||||
// This allows us to use XMLHttpRequest when running on the client to fetch it
|
||||
// Note: We use XMLHttpRequest instead of fetch so auth cookies are passed
|
||||
async getSession(forceUpdate) {
|
||||
// If running on the server, return session as will be loaded in constructor
|
||||
if (typeof window === 'undefined') {
|
||||
return new Promise(resolve => {
|
||||
resolve(this._session)
|
||||
})
|
||||
}
|
||||
|
||||
// If force update is set, clear data from store
|
||||
if (forceUpdate === true) {
|
||||
this._session = {}
|
||||
this._removeLocalStore('session')
|
||||
}
|
||||
|
||||
// Attempt to load session data from sessionStore on every call
|
||||
this._session = this._getLocalStore('session')
|
||||
|
||||
// If session data exists, has not expired AND forceUpdate is not set then
|
||||
// return the stored session we already have.
|
||||
if (this._session && Object.keys(this._session).length > 0 && this._session.expires && this._session.expires > Date.now()) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this._session)
|
||||
})
|
||||
}
|
||||
|
||||
// If we don't have session data, or it's expired, or forceUpdate is set
|
||||
// to true then revalidate it by fetching it again from the server.
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', '/session', true)
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
// Update session with session info
|
||||
this._session = JSON.parse(xhr.responseText)
|
||||
|
||||
// Set a value we will use to check this client should silently
|
||||
// revalidate based on the value of clientMaxAge set by the server
|
||||
this._session.expires = Date.now() + this._session.clientMaxAge
|
||||
|
||||
// Save changes to session
|
||||
this._saveLocalStore('session', this._session)
|
||||
|
||||
resolve(this._session)
|
||||
} else {
|
||||
reject(Error('XMLHttpRequest failed: Unable to get session'))
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
reject(Error('XMLHttpRequest error: Unable to get session'))
|
||||
}
|
||||
xhr.send()
|
||||
})
|
||||
}
|
||||
|
||||
async signin(username, password) {
|
||||
// Sign in to the server
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (typeof window === 'undefined') {
|
||||
return reject(Error('This method should only be called on the client'))
|
||||
}
|
||||
|
||||
// Make sure we have session in memory
|
||||
this._session = await this.getSession()
|
||||
|
||||
// Make sure we have the latest CSRF Token in our session
|
||||
this._session.csrfToken = await Session.getCsrfToken()
|
||||
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', '/login', true)
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
|
||||
xhr.onreadystatechange = async () => {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status !== 200) {
|
||||
return reject(Error('XMLHttpRequest error: Error while attempting to signin'))
|
||||
}
|
||||
|
||||
return resolve(true)
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
return reject(Error('XMLHttpRequest error: Unable to signin'))
|
||||
}
|
||||
xhr.send('_csrf=' + encodeURIComponent(this._session.csrfToken) + '&' +
|
||||
'username=' + encodeURIComponent(username) + '&' +
|
||||
'password=' + encodeURIComponent(password))
|
||||
})
|
||||
}
|
||||
|
||||
async signout() {
|
||||
// Signout from the server
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (typeof window === 'undefined') {
|
||||
return reject(Error('This method should only be called on the client'))
|
||||
}
|
||||
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', '/logout', true)
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
|
||||
xhr.onreadystatechange = async () => {
|
||||
if (xhr.readyState === 4) {
|
||||
// @TODO We aren't checking for success, just completion
|
||||
|
||||
// Update local session data
|
||||
this._session = await this.getSession(true)
|
||||
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
reject(Error('XMLHttpRequest error: Unable to signout'))
|
||||
}
|
||||
xhr.send('_csrf=' + encodeURIComponent(this._session.csrfToken))
|
||||
})
|
||||
}
|
||||
|
||||
// The Web Storage API is widely supported, but not always available (e.g.
|
||||
// it can be restricted in private browsing mode, triggering an exception).
|
||||
// We handle that silently by just returning null here.
|
||||
_getLocalStore(name) {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(name))
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
_saveLocalStore(name, data) {
|
||||
try {
|
||||
localStorage.setItem(name, JSON.stringify(data))
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
_removeLocalStore(name) {
|
||||
try {
|
||||
localStorage.removeItem(name)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,7 @@
|
|||
"connect-session-sequelize": "^4.1.0",
|
||||
"express": "^4.15.2",
|
||||
"express-session": "^1.15.2",
|
||||
"lusca": "^1.4.1",
|
||||
"next": "^2.3.1",
|
||||
"passport": "^0.3.2",
|
||||
"passport-local": "^1.0.0",
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react' // React import here just to keep 'xo' linter happy
|
||||
|
||||
export default() =>
|
||||
<div>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is the simplest possible example of a page.</p>
|
||||
<a href="/">Home</a>
|
||||
</div>
|
|
@ -1,23 +1,23 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Link from 'next/link';
|
||||
import DefaultPage from '../hocs/defaultPage';
|
||||
/**
|
||||
* The index page uses a layout page that pulls in header and footer components
|
||||
*/
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import Page from '../components/page'
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default class extends Page {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout session={this.props.session}>
|
||||
<h2>Under construction</h2>
|
||||
<ul>
|
||||
<li><Link prefetch href="/helloworld"><a>HelloWorld</a></Link> - The simplest possible example</li>
|
||||
<li><Link prefetch href="/login"><a>Login</a></Link> - prefetch</li>
|
||||
</ul>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
const Index = ({ isAuthenticated }) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello Worlds!</h1>
|
||||
{isAuthenticated && <p>Authenticated!!</p>}
|
||||
{!isAuthenticated && <p>Not Authriozed</p>}
|
||||
<p><Link prefetch href='/login'><a>Login</a></Link></p>
|
||||
<p><Link prefetch href='/logout'><a>Logout</a></Link></p>
|
||||
<p><Link prefetch href='/secret'><a>Secure Page</a></Link></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Index.propTypes = {
|
||||
isAuthenticated: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
export default DefaultPage(Index);
|
|
@ -1,25 +1,110 @@
|
|||
import React from 'react';
|
||||
import React from 'react'
|
||||
import Page from '../components/page'
|
||||
import Layout from '../components/layout'
|
||||
import Session from '../components/session'
|
||||
|
||||
export default class extends Page {
|
||||
|
||||
static async getInitialProps({req}) {
|
||||
// On the sign in page we always force get the latest session data from the
|
||||
// server by passing 'true' to getSession. This page is the destination
|
||||
// page after logging or linking/unlinking accounts so avoids any weird
|
||||
// edge cases.
|
||||
const session = new Session({req})
|
||||
return {session: await session.getSession(true)}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Get latest session data after rendering on client
|
||||
// Any page that is specified as the oauth callback should do this
|
||||
const session = new Session()
|
||||
this.state = {
|
||||
username: this.state.username,
|
||||
password: this.state.password,
|
||||
session: await session.getSession(true)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
session: this.props.session
|
||||
}
|
||||
this.handleSubmit = this.handleSubmit.bind(this)
|
||||
this.handleUsernameChange = this.handleUsernameChange.bind(this)
|
||||
this.handlePasswordChange = this.handlePasswordChange.bind(this)
|
||||
}
|
||||
|
||||
handleUsernameChange(event) {
|
||||
this.setState({
|
||||
username: event.target.value.trim(),
|
||||
password: this.state.password,
|
||||
session: this.state.session
|
||||
})
|
||||
}
|
||||
|
||||
handlePasswordChange(event) {
|
||||
this.setState({
|
||||
username: this.state.username,
|
||||
password: event.target.value.trim(),
|
||||
session: this.state.session
|
||||
})
|
||||
}
|
||||
|
||||
async handleSubmit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
const session = new Session()
|
||||
session.signin(this.state.username, this.state.password)
|
||||
.then(() => {
|
||||
// @FIXME next/router not working reliably so using window.location
|
||||
window.location = '/'
|
||||
})
|
||||
.catch(err => {
|
||||
// @FIXME Handle error
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
class Login extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
let signinForm = <div/>
|
||||
if (this.state.session.user) {
|
||||
signinForm = (
|
||||
<div>
|
||||
<form id='login' method='post' action='/login'>
|
||||
<div>
|
||||
<label>Username:</label>
|
||||
<input name='username' type='text' id='username'/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Password:</label>
|
||||
<input name='password' type='text' id='password'/>
|
||||
</div>
|
||||
<div>
|
||||
<input type='submit' value='Log In'/>
|
||||
</div>
|
||||
<h3>You are signed in</h3>
|
||||
<p>Name: <strong>{this.state.session.user.name}</strong></p>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
signinForm = (
|
||||
<div>
|
||||
<form id="signin" method="post" action="/login" onSubmit={this.handleSubmit}>
|
||||
<input name="_csrf" type="hidden" value={this.state.session.csrfToken}/>
|
||||
<h3>Sign in with email</h3>
|
||||
<p>
|
||||
<label htmlFor="username">Username</label><br/>
|
||||
<input name="username" type="text" placeholder="j.smith@example.com" id="username" value={this.state.username} onChange={this.handleUsernameChange}/>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="password">Password</label><br/>
|
||||
<input name="password" type="text" id="password" value={this.state.password} onChange={this.handlePasswordChange}/>
|
||||
</p>
|
||||
<p>
|
||||
<button id="submitButton" type="submit">Sign in</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default (Login);
|
||||
return (
|
||||
<Layout session={this.state.session}>
|
||||
<h2>Authentication</h2>
|
||||
{signinForm}
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ const session = require('express-session');
|
|||
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||
const passport = require('passport');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
const csrf = require('lusca').csrf();
|
||||
const models = require('../models');
|
||||
|
||||
exports.configure = ({
|
||||
|
@ -10,7 +11,8 @@ exports.configure = ({
|
|||
server = null,
|
||||
secret = 'change-me',
|
||||
store = new SequelizeStore({ db: models.sequelize, table: 'Session' }),
|
||||
maxAge = 60000 * 60 * 24 * 7 * 4 // 4 weeks
|
||||
maxAge = 60000 * 60 * 24 * 7 * 4, // 4 weeks
|
||||
clientMaxAge = 60000 // 60 seconds
|
||||
} = {}) => {
|
||||
if (!app) throw new Error('Null param')
|
||||
if (!server) throw new Error('Null param')
|
||||
|
@ -44,6 +46,10 @@ exports.configure = ({
|
|||
}
|
||||
}));
|
||||
|
||||
server.use((req, res, next) => {
|
||||
csrf(req, res, next);
|
||||
})
|
||||
|
||||
passport.use(new LocalStrategy((username, password, done) => {
|
||||
models.User.findOne({ where: {username: username} }).then(user => {
|
||||
if (!user) {
|
||||
|
@ -72,16 +78,32 @@ exports.configure = ({
|
|||
server.use(passport.initialize());
|
||||
server.use(passport.session());
|
||||
|
||||
server.get('/csrf', (req, res) => {
|
||||
return res.json({csrfToken: res.locals._csrf});
|
||||
})
|
||||
|
||||
server.get('/session', (req, res) => {
|
||||
let session = {
|
||||
clientMaxAge: clientMaxAge,
|
||||
csrfToken: res.locals._csrf
|
||||
}
|
||||
if (req.user) {
|
||||
session.user = req.user
|
||||
}
|
||||
|
||||
return res.json(session)
|
||||
})
|
||||
|
||||
server.post('/login',
|
||||
passport.authenticate('local', {
|
||||
failureRedirect: '/login',
|
||||
failureRedirect: '/error',
|
||||
}),
|
||||
(req, res) => {
|
||||
res.redirect('/')
|
||||
res.redirect('/');
|
||||
}
|
||||
);
|
||||
|
||||
server.get('/logout', (req, res) => {
|
||||
server.post('/logout', (req, res) => {
|
||||
req.logout();
|
||||
res.redirect('/');
|
||||
});
|
||||
|
|
|
@ -23,6 +23,13 @@ app.prepare()
|
|||
return handle(req, res);
|
||||
});
|
||||
|
||||
// Set vary header (good practice)
|
||||
// Note: This overrides any existing 'Vary' header but is okay in this app
|
||||
server.use(function (req, res, next) {
|
||||
res.setHeader('Vary', 'Accept-Encoding')
|
||||
next()
|
||||
});
|
||||
|
||||
server.listen(3000, err => {
|
||||
if (err) throw err;
|
||||
console.log('> Ready on http://localhost:3000');
|
||||
|
|
|
@ -1106,7 +1106,7 @@ core-js@^2.4.0:
|
|||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
core-util-is@^1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
|
||||
|
@ -2166,6 +2166,12 @@ lru-cache@^4.0.1:
|
|||
pseudomap "^1.0.1"
|
||||
yallist "^2.0.0"
|
||||
|
||||
lusca@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/lusca/-/lusca-1.4.1.tgz#498ad9edc4b77d858d0b1c237db8da7f850b36ec"
|
||||
dependencies:
|
||||
core-util-is "^1.0.2"
|
||||
|
||||
md5-file@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-3.1.1.tgz#db3c92c09bbda5c2de883fa5490dd711fddbbab9"
|
||||
|
@ -2718,13 +2724,13 @@ promise@^7.1.1:
|
|||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prop-types@15.5.7, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@~15.5.7:
|
||||
prop-types@15.5.7:
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.7.tgz#231c4f29cdd82e355011d4889386ca9059544dd1"
|
||||
dependencies:
|
||||
fbjs "^0.8.9"
|
||||
|
||||
prop-types@^15.5.10:
|
||||
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@~15.5.7:
|
||||
version "15.5.10"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in New Issue