use oauth2 password flow for webclient

This commit is contained in:
les 2020-01-27 00:47:03 +01:00
parent 6352cb3d12
commit b706333c85
21 changed files with 367 additions and 448 deletions

View file

@ -18,6 +18,7 @@
el-menu-item(v-if='!$auth.loggedIn' index='/login')
i.el-icon-user
span.hidden-xs-only {{$t('common.login')}}
el-submenu(v-if='$auth.loggedIn' index=3)
template(slot='title')
i.el-icon-user

View file

@ -48,7 +48,7 @@ export default {
data ({ $store }) {
return {
title: $store.state.settings.title,
description: $store.state.settings.description,
description: $store.state.settings.description
}
},
computed: {

View file

@ -67,15 +67,27 @@ module.exports = {
prefix: '/api'
},
auth: {
// localStorage: false, // https://github.com/nuxt-community/auth-module/issues/425
cookie: {
prefix: 'auth.',
expires: 360,
maxAge: 60 * 60 * 24 * 30
},
redirect: {
login: '/login'
login: '../login'
},
strategies: {
local: {
endpoints: {
login: { url: '/auth/login', method: 'post', propertyName: 'token' },
login: {
url: '../oauth/login',
method: 'post',
propertyName: 'access_token',
withCredentials: true,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
},
logout: false,
user: { url: '/auth/user', method: 'get', propertyName: false }
user: { url: '/user', method: 'get', propertyName: false }
},
tokenRequired: true,
tokenType: 'Bearer'

View file

@ -56,7 +56,7 @@
"@nuxtjs/auth": "^4.8.5",
"@nuxtjs/axios": "^5.9.3",
"accept-language": "^3.0.18",
"axios": "^0.19.1",
"axios": "^0.19.2",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.3",
"bootstrap": "^4.4.1",
@ -64,26 +64,22 @@
"consola": "^2.11.3",
"cookie-parser": "^1.4.4",
"cors": "^2.8.5",
"cross-env": "^6.0.0",
"cross-env": "^7.0.0",
"dayjs": "^1.8.19",
"element-ui": "^2.13.0",
"email-templates": "^7.0.1",
"email-templates": "^7.0.2",
"express": "^4.17.1",
"express-jwt": "^5.3.1",
"express-middleware-log": "^1.2.0",
"express-oauth-server": "^2.0.0",
"http-signature": "^1.3.1",
"ics": "^2.16.0",
"inquirer": "^7.0.3",
"inquirer": "^7.0.4",
"jsonwebtoken": "^8.5.1",
"less": "^3.10.3",
"lodash": "^4.17.14",
"mkdirp": "^0.5.1",
"modern-css-reset": "^1.0.4",
"mkdirp": "^1.0.3",
"moment-timezone": "^0.5.27",
"morgan": "^1.9.1",
"multer": "^1.4.2",
"node-fetch": "^2.6.0",
"nuxt": "^2.11.0",
"nuxt-express-module": "^0.0.11",
"pg": "^7.17.1",

View file

@ -22,7 +22,7 @@
</template>
<script>
import { mapActions, mapState } from 'vuex'
import { mapState } from 'vuex'
import { Message } from 'element-ui'
import get from 'lodash/get'
@ -45,10 +45,6 @@ export default {
this.$refs.email.focus()
},
methods: {
...mapActions(['login']),
close () {
this.$router.replace('/')
},
async forgot () {
if (!this.email) {
Message({ message: this.$t('login.insert_email'), showClose: true, type: 'error' })
@ -65,10 +61,15 @@ export default {
e.preventDefault()
try {
this.loading = true
await this.$auth.loginWith('local', { data: { email: this.email, password: this.password } })
const data = new URLSearchParams()
data.append('username', this.email)
data.append('password', this.password)
data.append('grant_type', 'password')
data.append('client_id', 'self')
await this.$auth.loginWith('local', { data })
this.loading = false
Message({ message: this.$t('login.ok'), showClose: true, type: 'success' })
this.close()
this.$router.replace('/')
} catch (e) {
Message({ message: this.$t('login.error') + this.$t(get(e, 'response.data.message', e)), showClose: true, type: 'error' })
this.loading = false

View file

@ -8,13 +8,12 @@
</template>
<script>
import { mapState } from 'vuex'
import { Message } from 'element-ui'
import debounce from 'lodash/debounce'
import url from 'url'
export default {
name: 'embedEvent',
name: 'EmbedEvent',
data () {
return {
instance_hostname: '',
@ -23,20 +22,7 @@ export default {
get_instance_info: debounce(this.getInstanceInfo, 500)
}
},
methods: {
getInstanceInfo () {
const instance_url = `https://${this.instance_hostname}/api/v1/instance`
fetch(instance_url)
.then( ret => ret.json())
.then(ret => {
this.instance = ret
this.proceed = true
})
.catch( e => {
this.proceed = false
})
}
},
computed: {
...mapState(['settings']),
domain () {
@ -45,7 +31,6 @@ export default {
},
couldGo () {
// check if is mastodon
const instance_url = `https://${this.instance_hostname}/api/v1/instance`
this.get_instance_info()
return true
},
@ -53,6 +38,19 @@ export default {
// check if exists
return `https://${this.instance_hostname}/authorize_interaction?uri=${this.settings.instance_name}@${this.domain}`
}
},
methods: {
getInstanceInfo () {
const instance_url = `https://${this.instance_hostname}/api/v1/instance`
this.$axios.$get(instance_url)
.then(ret => {
this.instance = ret
this.proceed = true
})
.catch(e => {
this.proceed = false
})
}
}
}
</script>
@ -60,4 +58,4 @@ export default {
.instance_thumb {
height: 20px;
}
</style>
</style>

View file

@ -13,19 +13,11 @@
</template>
<script>
import { mapState } from 'vuex'
import { Message, MessageBox } from 'element-ui'
import { MessageBox } from 'element-ui'
import url from 'url'
export default {
name: 'Settings',
async asyncData ({ $axios, params, error }) {
try {
const user = await $axios.$get('/auth/user')
return { user }
} catch (e) {
error({ statusCode: 404, message: 'Something goes wrong...' })
}
},
middleware: ['auth'],
data () {
return {

View file

@ -1,38 +1,35 @@
const { Op } = require('sequelize')
const { user: User } = require('./models')
const debug = require('debug')('auth')
const oauth = require('./oauth')
const Auth = {
/** isAuth middleware
* req.user is filled in server/helper.js#initMiddleware
*/
async isAuth (req, res, next) {
if (!req.user) {
return res
.status(403)
.send({ message: 'Failed to authenticate token ' })
}
req.user = await User.findOne({
where: { id: { [Op.eq]: req.user.id }, is_active: true }
})
if (!req.user) {
return res
.status(403)
.send({ message: 'Failed to authenticate token ' })
}
next()
isAuth (req, res, next) {
return oauth.oauthServer.authenticate()(req, res, next)
},
/** isAdmin middleware */
isAdmin (req, res, next) {
if (!req.user) {
return res
.status(403)
.send({ message: 'Failed to authenticate token ' })
oauth.oauthServer.authenticate()(req, res, () => {
req.user = res.locals.oauth.token.user
if (req.user.is_admin) {
next()
} else {
res.status(404)
}
})
},
hasPerm (scope) {
return (req, res, next) => {
debug(scope, req.path)
oauth.oauthServer.authenticate({ scope })(req, res, () => {
req.user = res.locals.oauth.token.user
next()
})
}
if (req.user.is_admin && req.user.is_active) { return next() }
return res.status(403).send({ message: 'Admin needed' })
}
}

View file

@ -9,23 +9,7 @@ const debug = require('debug')('controller:event')
const eventController = {
/** add a resource to event
* @todo not used anywhere, should we use with webmention?
* @todo should we use this for roply coming from fediverse?
*/
// async addComment (req, res) {
// // comments could be added to an event or to another comment
// let event = await Event.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } } })
// if (!event) {
// const comment = await Resource.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } }, include: Event })
// event = comment.event
// }
// const comment = new Comment(req.body)
// event.addComment(comment)
// res.json(comment)
// },
async getMeta (req, res) {
async _getMeta () {
const places = await Place.findAll({
order: [[Sequelize.literal('weigth'), 'DESC']],
attributes: {
@ -44,7 +28,11 @@ const eventController = {
}
})
res.json({ tags, places })
return { places, tags }
},
async getMeta (req, res) {
res.json(await eventController._getMeta())
},
async getNotifications (event, action) {
@ -197,131 +185,113 @@ const eventController = {
res.sendStatus(200)
},
async addRecurrent (start, places, where_tags, limit) {
const where = {
is_visible: true,
recurrent: { [Op.ne]: null }
// placeId: places
}
// async addRecurrent (start, places, where_tags, limit) {
// const where = {
// is_visible: true,
// recurrent: { [Op.ne]: null }
// // placeId: places
// }
const events = await Event.findAll({
where,
limit,
attributes: {
exclude: ['slug', 'likes', 'boost', 'userId', 'is_visible', 'description', 'createdAt', 'updatedAt', 'placeId']
},
order: ['start_datetime', [Tag, 'weigth', 'DESC']],
include: [
{ model: Resource, required: false, attributes: ['id'] },
{ model: Tag, ...where_tags, attributes: ['tag'], through: { attributes: [] } },
{ model: Place, required: false, attributes: ['id', 'name', 'address'] }
]
})
// const events = await Event.findAll({
// where,
// limit,
// attributes: {
// exclude: ['slug', 'likes', 'boost', 'userId', 'is_visible', 'description', 'createdAt', 'updatedAt', 'placeId']
// },
// order: ['start_datetime', [Tag, 'weigth', 'DESC']],
// include: [
// { model: Resource, required: false, attributes: ['id'] },
// { model: Tag, ...where_tags, attributes: ['tag'], through: { attributes: [] } },
// { model: Place, required: false, attributes: ['id', 'name', 'address'] }
// ]
// })
debug(`Found ${events.length} recurrent events`)
let allEvents = []
_.forEach(events, e => {
allEvents = allEvents.concat(eventController.createEventsFromRecurrent(e.get(), start))
})
// let allEvents = []
// _.forEach(events, e => {
// allEvents = allEvents.concat(eventController.createEventsFromRecurrent(e.get(), start))
// })
debug(`Created ${allEvents.length} events`)
return allEvents
},
// return allEvents
// },
// build singular events from a recurrent pattern
createEventsFromRecurrent (e, start, dueTo = null) {
const events = []
const recurrent = JSON.parse(e.recurrent)
if (!recurrent.frequency) { return false }
if (!dueTo) {
dueTo = moment.unix(start).add(2, 'month')
}
let cursor = moment.unix(start).startOf('week')
const start_date = moment.unix(e.start_datetime)
const duration = moment.unix(e.end_datetime).diff(start_date, 's')
const frequency = recurrent.frequency
const days = recurrent.days
const type = recurrent.type
// // build singular events from a recurrent pattern
// createEventsFromRecurrent (e, start, dueTo = null) {
// const events = []
// const recurrent = JSON.parse(e.recurrent)
// if (!recurrent.frequency) { return false }
// if (!dueTo) {
// dueTo = start.add(2, 'month')
// }
// let cursor = start.startOf('week')
// const start_date = moment.unix(e.start_datetime)
// const duration = moment.unix(e.end_datetime).diff(start_date, 's')
// const frequency = recurrent.frequency
// const days = recurrent.days
// const type = recurrent.type
// default frequency is '1d' => each day
const toAdd = { n: 1, unit: 'day' }
// // default frequency is '1d' => each day
// const toAdd = { n: 1, unit: 'day' }
// each week or 2 (search for the first specified day)
if (frequency === '1w' || frequency === '2w') {
cursor.add(days[0] - 1, 'day')
if (frequency === '2w') {
const nWeeks = cursor.diff(e.start_datetime, 'w') % 2
if (!nWeeks) { cursor.add(1, 'week') }
}
toAdd.n = Number(frequency[0])
toAdd.unit = 'week'
// cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
}
// // each week or 2 (search for the first specified day)
// if (frequency === '1w' || frequency === '2w') {
// cursor.add(days[0] - 1, 'day')
// if (frequency === '2w') {
// const nWeeks = cursor.diff(e.start_datetime, 'w') % 2
// if (!nWeeks) { cursor.add(1, 'week') }
// }
// toAdd.n = Number(frequency[0])
// toAdd.unit = 'week'
// // cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
// }
cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
// cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
// each month or 2
if (frequency === '1m' || frequency === '2m') {
// find first match
toAdd.n = 1
toAdd.unit = 'month'
if (type === 'weekday') {
// // each month or 2
// if (frequency === '1m' || frequency === '2m') {
// // find first match
// toAdd.n = 1
// toAdd.unit = 'month'
// if (type === 'weekday') {
} else if (type === 'ordinal') {
// } else if (type === 'ordinal') {
}
}
// }
// }
// add event at specified frequency
while (true) {
const first_event_of_week = cursor.clone()
days.forEach(d => {
if (type === 'ordinal') {
cursor.date(d)
} else {
cursor.day(d - 1)
}
if (cursor.isAfter(dueTo) || cursor.isBefore(start)) { return }
e.start_datetime = cursor.unix()
e.end_datetime = e.start_datetime + duration
events.push(Object.assign({}, e))
})
if (cursor.isAfter(dueTo)) { break }
cursor = first_event_of_week.add(toAdd.n, toAdd.unit)
cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
}
// // add event at specified frequency
// while (true) {
// const first_event_of_week = cursor.clone()
// days.forEach(d => {
// if (type === 'ordinal') {
// cursor.date(d)
// } else {
// cursor.day(d - 1)
// }
// if (cursor.isAfter(dueTo) || cursor.isBefore(start)) { return }
// e.start_datetime = cursor.unix()
// e.end_datetime = e.start_datetime + duration
// events.push(Object.assign({}, e))
// })
// if (cursor.isAfter(dueTo)) { break }
// cursor = first_event_of_week.add(toAdd.n, toAdd.unit)
// cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
// }
return events
},
// return events
// },
/**
* Select events based on params
*/
async select (req, res) {
const start = req.query.start || moment().unix()
const limit = req.query.limit || 100
const show_recurrent = req.query.show_recurrent || true
const filter_tags = req.query.tags || ''
const filter_places = req.query.places || ''
debug(`select limit:${limit} rec:${show_recurrent} tags:${filter_tags} places:${filter_places}`)
let where_tags = {}
async _select (start = moment.unix(), limit = 100, show_recurrent = true) {
const where = {
// confirmed event only
is_visible: true,
start_datetime: { [Op.gt]: start },
recurrent: null
start_datetime: { [Op.gt]: start }
}
if (filter_tags) {
where_tags = { where: { tag: filter_tags.split(',') } }
if (!show_recurrent) {
where.recurrent = null
}
if (filter_places) {
where.placeId = filter_places.split(',')
}
let events = await Event.findAll({
const events = await Event.findAll({
where,
limit,
attributes: {
@ -331,80 +301,77 @@ const eventController = {
order: ['start_datetime', [Tag, 'weigth', 'DESC']],
include: [
{ model: Resource, required: false, attributes: ['id'] },
{ model: Tag, ...where_tags, attributes: ['tag'], through: { attributes: [] } },
{ model: Tag, attributes: ['tag'], required: false, through: { attributes: [] } },
{ model: Place, required: false, attributes: ['id', 'name', 'address'] }
]
})
let recurrentEvents = []
events = _.map(events, e => e.get())
if (show_recurrent) {
recurrentEvents = await eventController.addRecurrent(start, where.placeId, where_tags, limit)
events = _.concat(events, recurrentEvents)
}
// flat tags
events = _(events).map(e => {
e.tags = e.tags.map(t => t.tag)
return _(events).map(e => {
e = e.get()
e.tags = e.tags ? e.tags.map(t => t && t.tag) : []
return e
})
// allEvents.sort((a,b) => a.start_datetime-b.start_datetime)
res.json(events.sort((a, b) => a.start_datetime - b.start_datetime))
// res.json(recurrentEvents)
},
/**
* Select events based on params
*/
async select (req, res) {
const start = req.query.start || moment().unix()
const limit = req.query.limit || 100
const show_recurrent = req.query.show_recurrent || true
res.json(await eventController._select(start, limit, show_recurrent))
// const filter_tags = req.query.tags || ''
// const filter_places = req.query.places || ''
// debug(`select limit:${limit} rec:${show_recurrent} tags:${filter_tags} places:${filter_places}`)
// let where_tags = {}
// const where = {
// // confirmed event only
// is_visible: true,
// start_datetime: { [Op.gt]: start },
// recurrent: null
// }
// if (filter_tags) {
// where_tags = { where: { tag: filter_tags.split(',') } }
// }
// if (filter_places) {
// where.placeId = filter_places.split(',')
// }
// let events = await Event.findAll({
// where,
// limit,
// attributes: {
// exclude: ['slug', 'likes', 'boost', 'userId', 'is_visible', 'description', 'createdAt', 'updatedAt', 'placeId']
// // include: [[Sequelize.fn('COUNT', Sequelize.col('activitypub_id')), 'ressources']]
// },
// order: ['start_datetime', [Tag, 'weigth', 'DESC']],
// include: [
// { model: Resource, required: false, attributes: ['id'] },
// { model: Tag, ...where_tags, attributes: ['tag'], through: { attributes: [] } },
// { model: Place, required: false, attributes: ['id', 'name', 'address'] }
// ]
// })
// let recurrentEvents = []
// events = _.map(events, e => e.get())
// if (show_recurrent) {
// recurrentEvents = await eventController.addRecurrent(moment.unix(start), where.placeId, where_tags, limit)
// events = _.concat(events, recurrentEvents)
// }
// // flat tags
// events = _(events).map(e => {
// e.tags = e.tags.map(t => t.tag)
// return e
// })
// res.json(events.sort((a, b) => a.start_datetime - b.start_datetime))
}
// async getAll (req, res) {
// // this is due how v-calendar shows dates
// const start = moment()
// .year(req.params.year)
// .month(req.params.month)
// .startOf('month')
// .startOf('week')
// let end = moment()
// .year(req.params.year)
// .month(req.params.month)
// .endOf('month')
// const shownDays = end.diff(start, 'days')
// if (shownDays <= 35) { end = end.add(1, 'week') }
// end = end.endOf('week')
// let events = await Event.findAll({
// where: {
// // return only confirmed events
// is_visible: true,
// [Op.or]: [
// // return all recurrent events regardless start_datetime
// { recurrent: { [Op.ne]: null } },
// // and events in specified range
// { start_datetime: { [Op.between]: [start.unix(), end.unix()] } }
// ]
// },
// attributes: { exclude: ['createdAt', 'updatedAt', 'placeId'] },
// order: [[Tag, 'weigth', 'DESC']],
// include: [
// { model: Resource, required: false, attributes: ['id'] },
// { model: Tag, required: false },
// { model: Place, required: false, attributes: ['id', 'name', 'address'] }
// ]
// })
// events = events.map(e => e.get()).map(e => {
// e.tags = e.tags.map(t => t.tag)
// return e
// })
// let allEvents = events.filter(e => !e.recurrent || e.recurrent.length === 0)
// events.filter(e => e.recurrent && e.recurrent.length).forEach(e => {
// const events = createEventsFromRecurrent(e, end)
// if (events) { allEvents = allEvents.concat(events) }
// })
// // allEvents.sort((a,b) => a.start_datetime-b.start_datetime)
// res.json(allEvents.sort((a, b) => a.start_datetime - b.start_datetime))
// }
}
module.exports = eventController

View file

@ -76,12 +76,12 @@ const oauthController = {
* */
async getAccessToken (accessToken) {
const oauth_token = await OAuthToken.findByPk(accessToken,
{ include: [User, { model: OAuthClient, as: 'client' }], nest: true, raw: true })
{ include: [User, { model: OAuthClient, as: 'client' }] })
return oauth_token
},
/**
* Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type.
* Invoked to retrieve a client using a client id or a client id/client secret combination, depend on the grant type.
*/
async getClient (client_id, client_secret) {
const client = await OAuthClient.findByPk(client_id, { raw: true })
@ -89,7 +89,7 @@ const oauthController = {
return false
}
if (client) { client.grants = ['authorization_code'] }
if (client) { client.grants = ['authorization_code', 'password'] }
return client
},
@ -119,11 +119,32 @@ const oauthController = {
return oauth_code.destroy()
},
async getUser (username, password) {
const user = await User.findOne({ where: { email: username } })
if (!user || !user.is_active) {
return false
}
// check if password matches
if (await user.comparePassword(password)) {
return user
}
return false
},
async saveAuthorizationCode (code, client, user) {
code.userId = user.id
code.oauthClientId = client.id
const ret = await OAuthCode.create(code)
return ret
},
verifyScope (token, scope) {
debug(token.user.is_admin)
if (token.user.is_admin) {
return true
} else {
return false
}
}
}

View file

@ -1,9 +1,7 @@
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const jwt = require('jsonwebtoken')
const { Op } = require('sequelize')
const jsonwebtoken = require('jsonwebtoken')
const sanitizeHtml = require('sanitize-html')
const config = require('config')
const mail = require('../mail')
@ -12,33 +10,6 @@ const settingsController = require('./settings')
const debug = require('debug')('user:controller')
const userController = {
async login (req, res) {
// find the user
const user = await User.findOne({ where: { email: req.body.email } })
if (!user) {
res.status(403).json({ success: false, message: 'auth.fail' })
} else if (user) {
if (!user.is_active) {
res.status(403).json({ success: false, message: 'auth.not_confirmed' })
// check if password matches
} else if (!await user.comparePassword(req.body.password)) {
res.status(403).json({ success: false, message: 'auth.fail' })
} else {
// if user is found and password is right
// create a token
const accessToken = jsonwebtoken.sign(
{
id: user.id,
email: user.email,
scope: [user.is_admin ? 'admin' : 'user']
},
config.secret
)
res.json({ token: accessToken })
}
}
},
async delEvent (req, res) {
const event = await Event.findByPk(req.params.id)
// check if event is mine (or user is admin)

View file

@ -2,7 +2,7 @@ const express = require('express')
const multer = require('multer')
const cors = require('cors')()
const { isAuth, isAdmin } = require('./auth')
const { isAuth, isAdmin, hasPerm } = require('./auth')
const eventController = require('./controller/event')
const exportController = require('./controller/export')
const userController = require('./controller/user')
@ -11,7 +11,6 @@ const instanceController = require('./controller/instance')
const apUserController = require('./controller/ap_user')
const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth')
const oauth = require('./oauth')
const storage = require('./storage')
const upload = multer({ storage })
@ -22,10 +21,9 @@ const api = express.Router()
api.use(express.urlencoded({ extended: false }))
api.use(express.json())
// AUTH
api.post('/auth/login', userController.login)
api.get('/auth/user', userController.current)
api.get('/user', isAuth, (req, res) => res.json(res.locals.oauth.token.user))
// api.post('/user/login', userController.login)
// api.get('/user/logout', userController.logout)
api.post('/user/recover', userController.forgotPassword)
api.post('/user/check_recover_code', userController.checkRecoverCode)
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
@ -35,12 +33,11 @@ api.post('/user/register', userController.register)
api.post('/user', isAdmin, userController.create)
// update user
api.put('/user', isAuth, userController.update)
api.put('/user', hasPerm('user:update'), userController.update)
// delete user
api.delete('/user/:id', isAdmin, userController.remove)
// api.delete('/user', userController.remove)
api.delete('/user', hasPerm('user:remove'), userController.remove)
// get all users
api.get('/users', isAdmin, userController.getAll)
@ -52,10 +49,10 @@ api.put('/place', isAdmin, eventController.updatePlace)
api.post('/user/event', upload.single('image'), userController.addEvent)
// update event
api.put('/user/event', isAuth, upload.single('image'), userController.updateEvent)
api.put('/user/event', hasPerm('event:write'), upload.single('image'), userController.updateEvent)
// remove event
api.delete('/user/event/:id', isAuth, userController.delEvent)
api.delete('/user/event/:id', hasPerm('event:remove'), userController.delEvent)
// get tags/places
api.get('/event/meta', eventController.getMeta)
@ -63,18 +60,17 @@ api.get('/event/meta', eventController.getMeta)
// get unconfirmed events
api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed)
// add event notification
// add event notification TODO
api.post('/event/notification', eventController.addNotification)
api.delete('/event/notification/:code', eventController.delNotification)
api.get('/settings', settingsController.getAllRequest)
api.post('/settings', isAdmin, settingsController.setRequest)
api.post('/settings/favicon', isAdmin, multer({ dest: 'thumb/' }).single('favicon'), settingsController.setFavicon)
// api.get('/settings/user_locale', settingsController.getUserLocale)
// confirm eventtags
api.get('/event/confirm/:event_id', isAuth, eventController.confirm)
api.get('/event/unconfirm/:event_id', isAuth, eventController.unconfirm)
// confirm event
api.get('/event/confirm/:event_id', hasPerm('event:write'), eventController.confirm)
api.get('/event/unconfirm/:event_id', hasPerm('event:write'), eventController.unconfirm)
// get event
api.get('/event/:event_id.:format?', cors, eventController.get)
@ -94,18 +90,11 @@ api.put('/resources/:resource_id', isAdmin, resourceController.hide)
api.delete('/resources/:resource_id', isAdmin, resourceController.remove)
api.get('/resources', isAdmin, resourceController.getAll)
api.get('/clients', isAuth, oauthController.getClients)
api.get('/client/:client_id', isAuth, oauthController.getClient)
api.get('/clients', hasPerm('oauth:read'), oauthController.getClients)
api.get('/client/:client_id', hasPerm('oauth:read'), oauthController.getClient)
api.post('/client', oauthController.createClient)
// api.get('/verify', oauth.oauthServer.authenticate(), (req, res) => {
// })
// Handle 404
api.use((req, res) => {
debug('404 Page not found: %s', req.path)
res.status(404).send('404: Page not Found')
})
api.use((req, res) => res.sendStatus(404))
// Handle 500
api.use((error, req, res, next) => {

View file

@ -32,7 +32,7 @@ const mail = {
updateFiles: false,
defaultLocale: settings.locale,
locale: settings.locale,
locales: ['it', 'es'] // TOFIX
locales: ['it', 'es', 'en', 'ca']
},
transport: config.smtp
})

View file

@ -10,6 +10,7 @@ const oauthServer = new OAuthServer({
useErrorHandler: true,
continueMiddleware: false,
debug: true,
requireClientAuthentication: { password: false },
authenticateHandler: {
handle (req) {
if (!req.user) {
@ -25,9 +26,12 @@ oauth.use(express.json())
oauth.use(express.urlencoded({ extended: false }))
oauth.post('/token', oauthServer.token())
oauth.post('/login', oauthServer.token())
oauth.get('/authorize', oauthServer.authorize())
oauth.use((req, res) => res.sendStatus(404))
oauth.use((err, req, res, next) => {
const error_msg = err.toString()
debug(err)

View file

@ -1,4 +1,4 @@
const fetch = require('node-fetch')
const fetch = require('axios')
// const request = require('request')
const crypto = require('crypto')
const config = require('config')
@ -36,18 +36,22 @@ const Helpers = {
const signature = signer.sign(privkey)
const signature_b64 = signature.toString('base64')
const header = `keyId="${config.baseurl}/federation/u/${settingsController.settings.instance_name}",headers="(request-target) host date",signature="${signature_b64}"`
const ret = await fetch(inbox, {
headers: {
Host: inboxUrl.hostname,
Date: d.toUTCString(),
Signature: header,
'Content-Type': 'application/activity+json; charset=utf-8',
Accept: 'application/activity+json, application/json; chartset=utf-8'
},
method: 'POST',
body: JSON.stringify(message)
})
debug('sign %s => %s', ret.status, await ret.text())
try {
const ret = await fetch(inbox, {
headers: {
Host: inboxUrl.hostname,
Date: d.toUTCString(),
Signature: header,
'Content-Type': 'application/activity+json; charset=utf-8',
Accept: 'application/activity+json, application/json; chartset=utf-8'
},
method: 'POST',
body: JSON.stringify(message)
})
debug('sign %s => %s', ret.status, await ret.text())
} catch (e) {
debug('ERROR ', e.toString())
}
},
async sendEvent (event, type = 'Create') {

View file

@ -82,4 +82,10 @@ router.get('/u/:name/outbox', Users.outbox)
router.get('/u/:name/followers', Users.followers)
router.get('/u/:name', Users.get)
// Handle 404
router.use((req, res) => {
debug('404 Page not found: %s', req.path)
res.status(404).send('404: Page not Found')
})
module.exports = router

View file

@ -1,27 +1,12 @@
const settingsController = require('./api/controller/settings')
const { user: User } = require('./api/models')
const acceptLanguage = require('accept-language')
const expressJwt = require('express-jwt')
const moment = require('moment-timezone')
const config = require('config')
const pkg = require('../package.json')
const jwt = expressJwt({
secret: config.secret,
credentialsRequired: false,
getToken: function fromHeaderOrQuerystring (req) {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1]
} else if (req.cookies && req.cookies['auth._token.local']) {
const [prefix, token] = req.cookies['auth._token.local'].split(' ')
if (prefix === 'Bearer') { return token }
}
return null
}
})
module.exports = {
async initMiddleware (req, res, next) {
async initSettings (req, res, next) {
await settingsController.load()
// initialize settings
req.settings = settingsController.settings
@ -40,12 +25,7 @@ module.exports = {
req.settings.user_locale = settingsController.user_locale[req.settings.locale]
moment.locale(req.settings.locale)
moment.tz.setDefault(req.settings.instance_timezone)
// TODO: oauth
jwt(req, res, async () => {
if (!req.user) { return next() }
req.user = await User.findOne({ where: { id: req.user.id, is_active: true } })
next()
})
next()
}
}

View file

@ -28,7 +28,7 @@ const notifier = {
},
async notifyEvent (action, eventId) {
const event = await Event.findByPk(eventId, {
include: [ Tag, Place, Notification, { model: User } ]
include: [Tag, Place, Notification, { model: User }]
})
debug('%s -> %s', action, event.title)

View file

@ -10,7 +10,10 @@ const webfinger = require('./federation/webfinger')
const { spamFilter } = require('./federation/helpers')
const debug = require('debug')('routes')
const exportController = require('./api/controller/export')
const eventController = require('./api/controller/event')
const helpers = require('./helpers')
const { startOfMonth, startOfWeek, getUnixTime } = require('date-fns')
const app = express()
app.use((req, res, next) => {
@ -26,8 +29,7 @@ app.use('/logo.png', express.static('./static/gancio.png'))
app.use('/media/', express.static(config.upload_path))
// initialize instance settings / authentication / locale
app.use(cookieParser())
app.use(helpers.initMiddleware)
app.use(helpers.initSettings)
app.use('/favicon.ico', (req, res, next) => {
const favicon_path = req.settings.favicon || config.favicon || './assets/favicon.ico'
return express.static(path.resolve(favicon_path))(req, res, next)
@ -36,14 +38,15 @@ app.use('/favicon.ico', (req, res, next) => {
// rss/ics/atom feed
app.get('/feed/:type', cors(), exportController.export)
// api!
app.use('/api', api)
app.use('/oauth', oauth)
// federation api / activitypub / webfinger / nodeinfo
app.use('/.well-known', webfinger)
app.use('/federation', federation)
// api!
app.use(cookieParser())
app.use('/api', api)
app.use('/oauth', oauth)
// // Handle 500
app.use((error, req, res, next) => {
debug('Error 500: %s', error)
@ -52,5 +55,12 @@ app.use((error, req, res, next) => {
// remaining request goes to nuxt
// first nuxt component is ./pages/index.vue (with ./layouts/default.vue)
// prefill current events, tags, places (used in every path)
app.use(async (req, res, next) => {
const start_datetime = getUnixTime(startOfWeek(startOfMonth(new Date())))
req.events = await eventController._select(start_datetime, 100, req.settings.recurrent_event_visible)
req.meta = await eventController._getMeta()
next()
})
module.exports = app

View file

@ -143,9 +143,6 @@ export const mutations = {
setLocale (state, locale) {
state.locale = locale
},
setUserLocale (state, user_locale) {
state.user_locale = user_locale
},
setPast (state, in_past) {
state.in_past = in_past
}
@ -154,25 +151,14 @@ export const mutations = {
export const actions = {
// this method is called server side only for each request for nuxt
// we use it to get configuration from db, set locale, etc...
async nuxtServerInit ({ commit }, { app, store, req }) {
if (req.user) { this.$auth.setUser(req.user) }
nuxtServerInit ({ commit }, { req }) {
commit('setSettings', req.settings)
const settings = req.settings
commit('setSettings', settings)
const start_datetime = moment().startOf('month').startOf('week').unix()
let query = `start=${start_datetime}`
if (settings.recurrent_event_visible) {
query += '&show_recurrent'
}
const events = await this.$axios.$get(`/event?${query}`)
commit('setEvents', events)
const { tags, places } = await this.$axios.$get('/event/meta')
store.commit('update', { tags, places })
commit('setEvents', req.events)
commit('update', req.meta)
// apply settings
commit('showRecurrentEvents', settings.allow_recurrent_event && settings.recurrent_event_visible)
commit('showRecurrentEvents', req.settings.allow_recurrent_event && req.settings.recurrent_event_visible)
},
async updateEvents ({ commit }, page) {
const [month, year] = [moment().month(), moment().year()]

114
yarn.lock
View file

@ -805,12 +805,12 @@
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.2.tgz#1c794cd6dbf2354d1eb1ef10e0303f573e1c7222"
integrity sha512-O4QDrx+JoGKZc6aN64L04vqa7e41tIiLU+OvKdcYaEMP97UttL0f9GIi9/0A4WAMx0uBd6SidDIhktZhgOcN8Q==
"@hapi/boom@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-8.0.1.tgz#13f1f2f2a3abfb0787c79e35e238c8aff6aa1661"
integrity sha512-SnBM2GzEYEA6AGFKXBqNLWXR3uNBui0bkmklYXX1gYtevVhDTy2uakwkSauxvIWMtlANGRhzChYg95If3FWCwA==
"@hapi/boom@^9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.0.0.tgz#28f9e77ecf2dea1fe3a8982b9d8827aa5137e33a"
integrity sha512-D+Or4yahLq3L7D1Jf0fR1+Lgr+HPK1lej8tc6hS/fBLmK66XdpvTyKv8YUR5ls1GeQy+KGtbpKAs+ZxyzNtUyA==
dependencies:
"@hapi/hoek" "8.x.x"
"@hapi/hoek" "9.x.x"
"@hapi/bourne@1.x.x":
version "1.3.2"
@ -822,6 +822,11 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.3.0.tgz#2b9db1cd00f3891005c77b3a8d608b88a6d0aa4d"
integrity sha512-C0QL9bmgUXTSuf8nDeGrpMjtJG7tPUr8wG6/wxPbP62tGwCwQtdMSJYfESowmY4P3Hn593f+8OzNY5bckcu/LQ==
"@hapi/hoek@9.x.x":
version "9.0.2"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.2.tgz#57597083f763eafbfdc902d16ec868aa787b24d2"
integrity sha512-LyibKv2QnD9BPI5g2L+g85yiIPv3ajYpENGFgy4u0xCLPhXWG1Zdx29neSB8sgX0/wz6k5TMjHzTwJ6+DaBYOA==
"@hapi/joi@^15.1.1":
version "15.1.1"
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7"
@ -839,17 +844,17 @@
dependencies:
"@hapi/hoek" "8.x.x"
"@ladjs/i18n@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@ladjs/i18n/-/i18n-3.0.1.tgz#efb32d7ee11421a48dbfda2c582113bb88c11713"
integrity sha512-/glfm9tY5Sd8L/oqnaFH1XrqfMFConHPxWPIioQ1SHrglmKOZEVFwN2Yc0zFs3Rzf9NXdTHMmsitgsyejDNtcQ==
"@ladjs/i18n@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@ladjs/i18n/-/i18n-3.0.2.tgz#4021164d42f5f69910776c2ddd6349e80caf6179"
integrity sha512-/GICRKaL8blp3xE1klDIDW1YZSw8CEqlwCn426dcagYrkv5RH+oYlWEQPRz6N0vJPeMaHk8DCFm9qppLjDOXuw==
dependencies:
"@hapi/boom" "^8.0.1"
"@hapi/boom" "^9.0.0"
boolean "3.0.0"
country-language "^0.1.7"
debug "^4.1.1"
i18n "^0.8.4"
i18n-locales "^0.0.2"
i18n-locales "^0.0.4"
lodash "^4.17.15"
moment "^2.24.0"
multimatch "^4.0.0"
@ -1819,11 +1824,6 @@ async-validator@~1.8.1:
dependencies:
babel-runtime "6.x"
async@^1.5.0:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
async@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/async/-/async-3.1.0.tgz#42b3b12ae1b74927b5217d8c0016baaf62463772"
@ -1884,6 +1884,13 @@ axios@^0.19.1:
dependencies:
follow-redirects "1.5.10"
axios@^0.19.2:
version "0.19.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
dependencies:
follow-redirects "1.5.10"
babel-eslint@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
@ -3077,12 +3084,12 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
cross-env@^6.0.0:
version "6.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941"
integrity sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==
cross-env@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.0.tgz#5a3b2ddce51ec713ea58f2fb79ce22e65b4f5479"
integrity sha512-rV6M9ldNgmwP7bx5u6rZsTbYidzwvrwIYZnT08hSGLcQCcggofgFW+sNe7IhA1SRauPS0QuLbbX+wdNtpqE5CQ==
dependencies:
cross-spawn "^7.0.0"
cross-spawn "^7.0.1"
cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
@ -3104,7 +3111,7 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^7.0.0:
cross-spawn@^7.0.0, cross-spawn@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14"
integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==
@ -3775,12 +3782,12 @@ elliptic@^6.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
email-templates@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/email-templates/-/email-templates-7.0.1.tgz#9361001fa7e6aafaa6ed55012ef79d991f2115b7"
integrity sha512-VY5RtLO7paSYYUDiDeGTIB4ejGXSwVTOhnPEQ5ZNyU3l0XhFCCIBtybXwu7Edn17MwRjhnOjyRR5SZUAAuVqVA==
email-templates@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/email-templates/-/email-templates-7.0.2.tgz#de7f848168b9c8bd11312c3220b2224a2fd45f73"
integrity sha512-nPP4AlFn9QoiphyEv3eS0VQ8Z7QzzMFI6PPYxmUeXOKeTlm9arDW8PLTylw4+40mO4rAHPmtmesM53mPgZooxA==
dependencies:
"@ladjs/i18n" "^3.0.1"
"@ladjs/i18n" "^3.0.2"
"@sindresorhus/is" "^1.2.0"
consolidate "^0.15.1"
debug "^4.1.1"
@ -4431,21 +4438,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
express-jwt@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/express-jwt/-/express-jwt-5.3.1.tgz#66f05c7dddb5409c037346a98b88965bb10ea4ae"
integrity sha512-1C9RNq0wMp/JvsH/qZMlg3SIPvKu14YkZ4YYv7gJQ1Vq+Dv8LH9tLKenS5vMNth45gTlEUGx+ycp9IHIlaHP/g==
dependencies:
async "^1.5.0"
express-unless "^0.3.0"
jsonwebtoken "^8.1.0"
lodash.set "^4.0.0"
express-middleware-log@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/express-middleware-log/-/express-middleware-log-1.2.0.tgz#62682021ba3b1cbfd6b081e7364ebb1fd6d5a0fb"
integrity sha512-1G9cHlGJs4+nFphSqVduJfCzeaqHeOdpTRBAjceRRcLWeHzj9sXDYP99tNjaeHsHn3N3vlNI+vIn/lb9eYXmuw==
express-oauth-server@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/express-oauth-server/-/express-oauth-server-2.0.0.tgz#57b08665c1201532f52c4c02f19709238b99a48d"
@ -4455,11 +4447,6 @@ express-oauth-server@^2.0.0:
express "^4.13.3"
oauth2-server "3.0.0"
express-unless@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/express-unless/-/express-unless-0.3.1.tgz#2557c146e75beb903e2d247f9b5ba01452696e20"
integrity sha1-JVfBRudb65A+LSR/m1ugFFJpbiA=
express@^4.13.3, express@^4.16.3, express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@ -5432,10 +5419,12 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
i18n-locales@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/i18n-locales/-/i18n-locales-0.0.2.tgz#12e56046f1fa260e11658f4ac62f60b363479ff9"
integrity sha512-WCaJVIfU10v0/ZNy+mG7fCUQb1o2PsM7tNf1dUg0uU9OxtygDkWRqLT9Q/X30V2XsUb6XUEPbSsdUiORfDPVQA==
i18n-locales@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/i18n-locales/-/i18n-locales-0.0.4.tgz#95d6505f6563f870f68860c23d35f82bd805cbf5"
integrity sha512-aP6VjhoBwSC8uZUehHWSszqdeWiheNXp0+oLPcZY4QAktsqcouHNYQee2NQFM4KNcCTKHHbfXrRUuOxjxF2jYw==
dependencies:
country-language "^0.1.7"
i18n@^0.8.4:
version "0.8.4"
@ -5642,10 +5631,10 @@ inquirer@^7.0.0:
strip-ansi "^5.1.0"
through "^2.3.6"
inquirer@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.3.tgz#f9b4cd2dff58b9f73e8d43759436ace15bed4567"
integrity sha512-+OiOVeVydu4hnCGLCSX+wedovR/Yzskv9BFqUNNKq9uU2qg7LCcCo3R86S2E7WLo0y/x2pnEZfZe1CoYnORUAw==
inquirer@^7.0.4:
version "7.0.4"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703"
integrity sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==
dependencies:
ansi-escapes "^4.2.1"
chalk "^2.4.2"
@ -6138,7 +6127,7 @@ jsonfile@^4.0.0:
optionalDependencies:
graceful-fs "^4.1.6"
jsonwebtoken@^8.1.0, jsonwebtoken@^8.5.1:
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
@ -6538,11 +6527,6 @@ lodash.reject@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415"
integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=
lodash.set@^4.0.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
lodash.snakecase@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d"
@ -7044,10 +7028,10 @@ mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
dependencies:
minimist "0.0.8"
modern-css-reset@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/modern-css-reset/-/modern-css-reset-1.0.4.tgz#4a1fff644cd0b314b048db23e16bccb9b7411caf"
integrity sha512-HspLib3vUdgdGNV9JcBrQXlqiWB/tGr9oMaSiqGR7+J1kQmJbxlZzp/3ufDR3lZLwDlWDuMh2VtvSY+Aa0xNow==
mkdirp@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea"
integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==
moment-timezone@^0.5.21:
version "0.5.26"