start with nuxt
This commit is contained in:
48
server/api/auth.js
Normal file
48
server/api/auth.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { Op } = require('sequelize')
|
||||
const config = require('./config')
|
||||
const User = require('./models/user')
|
||||
|
||||
const Auth = {
|
||||
fillUser(req, res, next) {
|
||||
const token =
|
||||
req.body.token || req.params.token || req.headers['x-access-token']
|
||||
if (!token) return next()
|
||||
jwt.verify(token, config.secret, async (err, decoded) => {
|
||||
if (err) return next()
|
||||
req.user = await User.findOne({
|
||||
where: { email: { [Op.eq]: decoded.email }, is_active: true }
|
||||
})
|
||||
next()
|
||||
})
|
||||
},
|
||||
isAuth(req, res, next) {
|
||||
const token =
|
||||
(req.body && req.body.token) ||
|
||||
req.params.token ||
|
||||
req.headers['x-access-token']
|
||||
if (!token) return res.status(403).send({ message: 'Token not found' })
|
||||
jwt.verify(token, config.secret, async (err, decoded) => {
|
||||
if (err) {
|
||||
return res
|
||||
.status(403)
|
||||
.send({ message: 'Failed to authenticate token ' + err })
|
||||
}
|
||||
req.user = await User.findOne({
|
||||
where: { email: { [Op.eq]: decoded.email }, is_active: true }
|
||||
})
|
||||
if (!req.user) {
|
||||
return res
|
||||
.status(403)
|
||||
.send({ message: 'Failed to authenticate token ' + err })
|
||||
}
|
||||
next()
|
||||
})
|
||||
},
|
||||
isAdmin(req, res, next) {
|
||||
if (req.user.is_admin && req.user.is_active) return next()
|
||||
return res.status(403).send({ message: 'Admin needed' })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Auth
|
||||
27
server/api/config.js
Normal file
27
server/api/config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/* backend configuration */
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
const db = require('./config/config.json')[env]
|
||||
|
||||
module.exports = {
|
||||
locale: process.env.LOCALE || 'it',
|
||||
title: process.env.TITLE || 'GANCIO',
|
||||
description: process.env.DESCRIPTION || 'A calendar for radical communities',
|
||||
baseurl: process.env.BASE_URL || 'http://localhost:8080',
|
||||
apiurl:
|
||||
env === 'production'
|
||||
? process.env.BASE_URL + '/api'
|
||||
: 'http://localhost:9000',
|
||||
db,
|
||||
admin: process.env.ADMIN_EMAIL,
|
||||
|
||||
smtp: {
|
||||
host: process.env.SMTP_HOST,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
}
|
||||
},
|
||||
|
||||
secret: process.env.SECRET || 'notsosecret'
|
||||
}
|
||||
14
server/api/config/config.json
Normal file
14
server/api/config/config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"development": {
|
||||
"storage": "/home/les/dev/hacklab/gancio/db.sqlite",
|
||||
"dialect": "sqlite",
|
||||
"logging": false
|
||||
},
|
||||
"production": {
|
||||
"username": "docker",
|
||||
"password": "docker",
|
||||
"database": "gancio",
|
||||
"host": "db",
|
||||
"dialect": "postgres"
|
||||
}
|
||||
}
|
||||
82
server/api/controller/bot.js
Normal file
82
server/api/controller/bot.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// const { User, Event, Comment, Tag } = require('../model')
|
||||
const config = require('../config')
|
||||
const Mastodon = require('mastodon-api')
|
||||
// const Sequelize = require('sequelize')
|
||||
// const Op = Sequelize.Op
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const moment = require('moment')
|
||||
moment.locale('it')
|
||||
|
||||
const botController = {
|
||||
bots: [],
|
||||
// async initialize () {
|
||||
// console.log('initialize bots')
|
||||
// const botUsers = await User.findAll({ where: { mastodon_auth: { [Op.ne]: null } } })
|
||||
// console.log(botUsers)
|
||||
// botController.bots = botUsers.map(user => {
|
||||
// console.log('initialize bot ', user.name)
|
||||
// console.log('.. ', user.mastodon_auth)
|
||||
// const { client_id, client_secret, access_token } = user.mastodon_auth
|
||||
// const bot = new Mastodon({ access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
|
||||
// const listener = bot.stream('streaming/direct')
|
||||
// listener.on('message', botController.message)
|
||||
// listener.on('error', botController.error)
|
||||
// return { email: user.email, bot }
|
||||
// })
|
||||
// console.log(botController.bots)
|
||||
// },
|
||||
// add (user, token) {
|
||||
// const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
|
||||
// const listener = bot.stream('streaming/direct')
|
||||
// listener.on('message', botController.message)
|
||||
// listener.on('error', botController.error)
|
||||
// botController.bots.push({ email: user.email, bot })
|
||||
// },
|
||||
async post (mastodon_auth, event) {
|
||||
const { access_token, instance } = mastodon_auth
|
||||
const bot = new Mastodon({ access_token, api_url: `https://${instance}/api/v1/` })
|
||||
const status = `${event.title} @ ${event.place.name} ${moment(event.start_datetime).format('ddd, D MMMM HH:mm')} -
|
||||
${event.description.length > 200 ? event.description.substr(0, 200) + '...' : event.description} - ${event.tags.map(t => '#' + t.tag).join(' ')} ${config.baseurl}/event/${event.id}`
|
||||
|
||||
let media
|
||||
if (event.image_path) {
|
||||
const file = path.join(__dirname, '..', '..', 'uploads', event.image_path)
|
||||
if (fs.statSync(file)) {
|
||||
media = await bot.post('media', { file: fs.createReadStream(file) })
|
||||
}
|
||||
}
|
||||
return bot.post('statuses', { status, visibility: 'public', media_ids: media ? [media.data.id] : [] })
|
||||
}
|
||||
// async message (msg) {
|
||||
// console.log(msg)
|
||||
// console.log(msg.data.accounts)
|
||||
// const replyid = msg.data.in_reply_to_id || msg.data.last_status.in_reply_to_id
|
||||
// if (!replyid) return
|
||||
// const event = await Event.findOne({ where: { activitypub_id: replyid } })
|
||||
// if (!event) {
|
||||
// check for comment..
|
||||
// const comment = await Comment.findOne( {where: { }})
|
||||
// }
|
||||
// const comment = await Comment.create({activitypub_id: msg.data.last_status.id, text: msg.data.last_status.content, author: msg.data.accounts[0].username })
|
||||
// event.addComment(comment)
|
||||
// console.log(event)
|
||||
// const comment = await Comment.findOne( { where: {activitypub_id: msg.data.in_reply_to}} )
|
||||
// console.log('dentro message ', data)
|
||||
|
||||
// add comment to specified event
|
||||
// let comment
|
||||
// if (!event) {
|
||||
// const comment = await Comment.findOne({where: {activitypub_id: req.body.id}, include: Event})
|
||||
// event = comment.event
|
||||
// }
|
||||
// const comment = new Comment(req.body)
|
||||
// event.addComment(comment)
|
||||
// },
|
||||
// error (err) {
|
||||
// console.log('error ', err)
|
||||
// }
|
||||
}
|
||||
|
||||
// setTimeout(botController.initialize, 2000)
|
||||
module.exports = botController
|
||||
169
server/api/controller/event.js
Normal file
169
server/api/controller/event.js
Normal file
@@ -0,0 +1,169 @@
|
||||
const crypto = require('crypto')
|
||||
const moment = require('moment')
|
||||
const { Op } = require('sequelize')
|
||||
const lodash = require('lodash')
|
||||
const { User, Event, Comment, Tag, Place, Notification } = require('../model')
|
||||
|
||||
const eventController = {
|
||||
|
||||
async addComment(req, res) {
|
||||
// comment 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 Comment.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) {
|
||||
const places = await Place.findAll()
|
||||
const tags = await Tag.findAll()
|
||||
res.json({ tags, places })
|
||||
},
|
||||
|
||||
async getNotifications(event) {
|
||||
function match(event, filters) {
|
||||
// matches if no filter specified
|
||||
if (!filters) return true
|
||||
|
||||
// check for visibility
|
||||
if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) return false
|
||||
|
||||
if (!filters.tags && !filters.places) return true
|
||||
if (!filters.tags.length && !filters.places.length) return true
|
||||
if (filters.tags.length) {
|
||||
const m = lodash.intersection(event.tags.map(t => t.tag), filters.tags)
|
||||
if (m.length > 0) return true
|
||||
}
|
||||
if (filters.places.length) {
|
||||
if (filters.places.find(p => p === event.place.name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
const notifications = await Notification.findAll()
|
||||
|
||||
// get notification that matches with selected event
|
||||
return notifications.filter(notification => match(event, notification.filters))
|
||||
},
|
||||
|
||||
async updateTag(req, res) {
|
||||
const tag = await Tag.findByPk(req.body.tag)
|
||||
if (tag) {
|
||||
res.json(await tag.update(req.body))
|
||||
} else {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
||||
async updatePlace(req, res) {
|
||||
const place = await Place.findByPk(req.body.id)
|
||||
await place.update(req.body)
|
||||
res.json(place)
|
||||
},
|
||||
|
||||
async get(req, res) {
|
||||
const id = req.params.event_id
|
||||
const event = await Event.findByPk(id, { include: [User, Tag, Comment, Place] })
|
||||
res.json(event)
|
||||
},
|
||||
|
||||
async confirm(req, res) {
|
||||
const id = req.params.event_id
|
||||
const event = await Event.findByPk(id)
|
||||
|
||||
try {
|
||||
await event.update({ is_visible: true })
|
||||
// insert notification
|
||||
const notifications = await eventController.getNotifications(event)
|
||||
await event.setNotifications(notifications)
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
||||
async unconfirm(req, res) {
|
||||
const id = req.params.event_id
|
||||
const event = await Event.findByPk(id)
|
||||
|
||||
try {
|
||||
await event.update({ is_visible: false })
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
||||
async getUnconfirmed(req, res) {
|
||||
const events = await Event.findAll({
|
||||
where: {
|
||||
is_visible: false
|
||||
},
|
||||
order: [['start_datetime', 'ASC']],
|
||||
include: [Tag, Place]
|
||||
})
|
||||
res.json(events)
|
||||
},
|
||||
|
||||
async addNotification(req, res) {
|
||||
try {
|
||||
const notification = {
|
||||
filters: { is_visible: true },
|
||||
email: req.body.email,
|
||||
type: 'mail',
|
||||
remove_code: crypto.randomBytes(16).toString('hex')
|
||||
}
|
||||
await Notification.create(notification)
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
||||
async delNotification(req, res) {
|
||||
const remove_code = req.params.code
|
||||
try {
|
||||
const notification = await Notification.findOne({ where: { remove_code: { [Op.eq]: remove_code } } })
|
||||
await notification.destroy()
|
||||
} catch (e) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
res.sendStatus(200)
|
||||
},
|
||||
|
||||
async getAll(req, res) {
|
||||
console.log('sono qui dentro !')
|
||||
// this is due how v-calendar shows dates
|
||||
const start = moment().year(req.params.year).month(req.params.month)
|
||||
.startOf('month').startOf('isoWeek')
|
||||
let end = moment().year(req.params.year).month(req.params.month).endOf('month')
|
||||
const shownDays = end.diff(start, 'days')
|
||||
if (shownDays <= 34) end = end.add(1, 'week')
|
||||
end = end.endOf('isoWeek')
|
||||
const events = await Event.findAll({
|
||||
// where: {
|
||||
// is_visible: true,
|
||||
// [Op.and]: [
|
||||
// { start_datetime: { [Op.gte]: start } },
|
||||
// { start_datetime: { [Op.lte]: end } }
|
||||
// ]
|
||||
// },
|
||||
// order: [['start_datetime', 'ASC']],
|
||||
// include: [
|
||||
// { model: User, required: false },
|
||||
// Comment,
|
||||
// Tag,
|
||||
// { model: Place, required: false }
|
||||
// ]
|
||||
})
|
||||
console.log(events)
|
||||
res.json(events)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = eventController
|
||||
64
server/api/controller/export.js
Normal file
64
server/api/controller/export.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const { Event, Comment, Tag, Place } = require('../model')
|
||||
const { Op } = require('sequelize')
|
||||
const config = require('../config')
|
||||
const moment = require('moment')
|
||||
const ics = require('ics')
|
||||
|
||||
const exportController = {
|
||||
|
||||
async export (req, res) {
|
||||
console.log('type ', req.params.type)
|
||||
const type = req.params.type
|
||||
const tags = req.query.tags
|
||||
const places = req.query.places
|
||||
const whereTag = {}
|
||||
const wherePlace = {}
|
||||
const yesterday = moment().subtract('1', 'day')
|
||||
if (tags) {
|
||||
whereTag.tag = tags.split(',')
|
||||
}
|
||||
if (places) {
|
||||
wherePlace.name = places.split(',')
|
||||
}
|
||||
const events = await Event.findAll({
|
||||
where: { is_visible: true, start_datetime: { [Op.gte]: yesterday } },
|
||||
include: [Comment, {
|
||||
model: Tag,
|
||||
where: whereTag
|
||||
}, { model: Place, where: wherePlace } ]
|
||||
})
|
||||
switch (type) {
|
||||
case 'feed':
|
||||
return exportController.feed(res, events.slice(0, 20))
|
||||
case 'ics':
|
||||
return exportController.ics(res, events)
|
||||
}
|
||||
},
|
||||
|
||||
async feed (res, events) {
|
||||
res.type('application/rss+xml; charset=UTF-8')
|
||||
res.render('feed/rss.pug', { events, config, moment })
|
||||
},
|
||||
|
||||
async ics (res, events) {
|
||||
const eventsMap = events.map(e => {
|
||||
const tmpStart = moment(e.start_datetime)
|
||||
const tmpEnd = moment(e.end_datetime)
|
||||
const start = [tmpStart.year(), tmpStart.month() + 1, tmpStart.date(), tmpStart.hour(), tmpStart.minute()]
|
||||
const end = [tmpEnd.year(), tmpEnd.month() + 1, tmpEnd.date(), tmpEnd.hour(), tmpEnd.minute()]
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
title: e.title,
|
||||
description: e.description,
|
||||
location: e.place.name + ' ' + e.place.address
|
||||
}
|
||||
})
|
||||
res.type('text/calendar; charset=UTF-8')
|
||||
const { error, value } = ics.createEvents(eventsMap)
|
||||
console.log(error, value)
|
||||
res.send(value)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exportController
|
||||
27
server/api/controller/settings.js
Normal file
27
server/api/controller/settings.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const { Settings } = require('../model')
|
||||
|
||||
const settingsController = {
|
||||
async setAdminSetting (key, value) {
|
||||
await Settings.findOrCreate({ where: { key },
|
||||
defaults: { value } })
|
||||
.spread((settings, created) => {
|
||||
if (!created) return settings.update({ value })
|
||||
})
|
||||
},
|
||||
|
||||
async getAdminSettings (req, res) {
|
||||
const settings = await settingsController.settings()
|
||||
res.json(settings)
|
||||
},
|
||||
|
||||
async settings () {
|
||||
const settings = await Settings.findAll()
|
||||
const map = {}
|
||||
settings.forEach(setting => {
|
||||
map[setting.key] = setting.value
|
||||
})
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = settingsController
|
||||
283
server/api/controller/user.js
Normal file
283
server/api/controller/user.js
Normal file
@@ -0,0 +1,283 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const Mastodon = require('mastodon-api')
|
||||
|
||||
const User = require('../models/user')
|
||||
const { Event, Tag, Place } = require('../models/event')
|
||||
const settingsController = require('./settings')
|
||||
const eventController = require('./event')
|
||||
const config = require('../config')
|
||||
const mail = require('../mail')
|
||||
const { Op } = require('sequelize')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const userController = {
|
||||
async login (req, res) {
|
||||
// find the user
|
||||
const user = await User.findOne({ where: { email: { [Op.eq]: req.body.email } } })
|
||||
if (!user) {
|
||||
res.status(404).json({ success: false, message: 'AUTH_FAIL' })
|
||||
} else if (user) {
|
||||
if (!user.is_active) {
|
||||
res.status(403).json({ success: false, message: '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 payload = { email: user.email }
|
||||
var token = jwt.sign(payload, config.secret)
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Enjoy your token!',
|
||||
token,
|
||||
user
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async setToken (req, res) {
|
||||
req.user.mastodon_auth = req.body
|
||||
await req.user.save()
|
||||
res.json(req.user)
|
||||
},
|
||||
|
||||
async delEvent (req, res) {
|
||||
const event = await Event.findByPk(req.params.id)
|
||||
// check if event is mine (or user is admin)
|
||||
if (event && (req.user.is_admin || req.user.id === event.userId)) {
|
||||
if (event.image_path) {
|
||||
const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path)
|
||||
const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path)
|
||||
await fs.unlink(old_path)
|
||||
await fs.unlink(old_thumb_path)
|
||||
}
|
||||
await event.destroy()
|
||||
res.sendStatus(200)
|
||||
} else {
|
||||
res.sendStatus(403)
|
||||
}
|
||||
},
|
||||
|
||||
// ADD EVENT
|
||||
async addEvent (req, res) {
|
||||
const body = req.body
|
||||
|
||||
// remove description tag and create anchor tags
|
||||
const description = body.description
|
||||
.replace(/(<([^>]+)>)/ig, '')
|
||||
.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1">$1</a>')
|
||||
|
||||
const eventDetails = {
|
||||
title: body.title,
|
||||
description,
|
||||
multidate: body.multidate,
|
||||
start_datetime: body.start_datetime,
|
||||
end_datetime: body.end_datetime,
|
||||
is_visible: !!req.user
|
||||
}
|
||||
|
||||
if (req.file) {
|
||||
eventDetails.image_path = req.file.filename
|
||||
}
|
||||
|
||||
let event = await Event.create(eventDetails)
|
||||
|
||||
// create place
|
||||
let place
|
||||
try {
|
||||
place = await Place.findOrCreate({ where: { name: body.place_name },
|
||||
defaults: { address: body.place_address } })
|
||||
.spread((place, created) => place)
|
||||
await event.setPlace(place)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
// create/assign tags
|
||||
if (body.tags) {
|
||||
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
|
||||
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
|
||||
await event.addTags(tags)
|
||||
}
|
||||
if (req.user) await req.user.addEvent(event)
|
||||
event = await Event.findByPk(event.id, { include: [User, Tag, Place] })
|
||||
|
||||
// insert notifications
|
||||
const notifications = await eventController.getNotifications(event)
|
||||
await event.setNotifications(notifications)
|
||||
|
||||
return res.json(event)
|
||||
},
|
||||
|
||||
async updateEvent (req, res) {
|
||||
const body = req.body
|
||||
const event = await Event.findByPk(body.id)
|
||||
if (!req.user.is_admin && event.userId !== req.user.id) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (req.file) {
|
||||
if (event.image_path) {
|
||||
const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path)
|
||||
const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path)
|
||||
await fs.unlink(old_path, e => console.error(e))
|
||||
await fs.unlink(old_thumb_path, e => console.error(e))
|
||||
}
|
||||
body.image_path = req.file.filename
|
||||
}
|
||||
|
||||
body.description = body.description
|
||||
.replace(/(<([^>]+)>)/ig, '') // remove all tags from description
|
||||
.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1">$1</a>') // add links
|
||||
|
||||
await event.update(body)
|
||||
let place
|
||||
try {
|
||||
place = await Place.findOrCreate({ where: { name: body.place_name },
|
||||
defaults: { address: body.place_address } })
|
||||
.spread((place, created) => place)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
await event.setPlace(place)
|
||||
await event.setTags([])
|
||||
if (body.tags) {
|
||||
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
|
||||
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
|
||||
await event.addTags(tags)
|
||||
}
|
||||
const newEvent = await Event.findByPk(event.id, { include: [User, Tag, Place] })
|
||||
return res.json(newEvent)
|
||||
},
|
||||
|
||||
async getAuthURL (req, res) {
|
||||
const instance = req.body.instance
|
||||
const is_admin = req.body.admin && req.user.is_admin
|
||||
const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}`
|
||||
const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`,
|
||||
config.title, 'read write', callback)
|
||||
const url = await Mastodon.getAuthorizationUrl(client_id, client_secret,
|
||||
`https://${instance}`, 'read write', callback)
|
||||
|
||||
if (is_admin) {
|
||||
await settingsController.setAdminSetting('mastodon_auth', { client_id, client_secret, instance })
|
||||
} else {
|
||||
req.user.mastodon_auth = { client_id, client_secret, instance }
|
||||
await req.user.save()
|
||||
}
|
||||
res.json(url)
|
||||
},
|
||||
|
||||
async code (req, res) {
|
||||
const { code, is_admin } = req.body
|
||||
let client_id, client_secret, instance
|
||||
const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}`
|
||||
|
||||
if (is_admin) {
|
||||
const settings = await settingsController.settings();
|
||||
({ client_id, client_secret, instance } = settings.mastodon_auth)
|
||||
} else {
|
||||
({ client_id, client_secret, instance } = req.user.mastodon_auth)
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await Mastodon.getAccessToken(client_id, client_secret, code,
|
||||
`https://${instance}`, callback)
|
||||
const mastodon_auth = { client_id, client_secret, access_token: token, instance }
|
||||
if (is_admin) {
|
||||
await settingsController.setAdminSetting('mastodon_auth', mastodon_auth)
|
||||
res.json(instance)
|
||||
} else {
|
||||
req.user.mastodon_auth = mastodon_auth
|
||||
await req.user.save()
|
||||
// await bot.add(req.user, token)
|
||||
res.json(req.user)
|
||||
}
|
||||
} catch (e) {
|
||||
res.json(e)
|
||||
}
|
||||
},
|
||||
|
||||
async forgotPassword (req, res) {
|
||||
const email = req.body.email
|
||||
const user = await User.findOne({ where: { email: { [Op.eq]: email } } })
|
||||
if (!user) return res.sendStatus(200)
|
||||
|
||||
user.recover_code = crypto.randomBytes(16).toString('hex')
|
||||
mail.send(user.email, 'recover', { user, config })
|
||||
await user.save()
|
||||
res.sendStatus(200)
|
||||
},
|
||||
|
||||
async checkRecoverCode (req, res) {
|
||||
const recover_code = req.body.recover_code
|
||||
if (!recover_code) return res.sendStatus(400)
|
||||
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
|
||||
if (!user) return res.sendStatus(400)
|
||||
res.json(user)
|
||||
},
|
||||
|
||||
async updatePasswordWithRecoverCode (req, res) {
|
||||
const recover_code = req.body.recover_code
|
||||
if (!recover_code) return res.sendStatus(400)
|
||||
const password = req.body.password
|
||||
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
|
||||
if (!user) return res.sendStatus(400)
|
||||
user.password = password
|
||||
await user.save()
|
||||
res.sendStatus(200)
|
||||
},
|
||||
|
||||
async current (req, res) {
|
||||
res.json(req.user)
|
||||
},
|
||||
|
||||
async getAll (req, res) {
|
||||
const users = await User.findAll({
|
||||
order: [['createdAt', 'DESC']]
|
||||
})
|
||||
res.json(users)
|
||||
},
|
||||
|
||||
async update (req, res) {
|
||||
const user = await User.findByPk(req.body.id)
|
||||
if (user) {
|
||||
if (!user.is_active && req.body.is_active) {
|
||||
await mail.send(user.email, 'confirm', { user, config })
|
||||
}
|
||||
await user.update(req.body)
|
||||
res.json(user)
|
||||
} else {
|
||||
res.sendStatus(400)
|
||||
}
|
||||
},
|
||||
|
||||
async register (req, res) {
|
||||
const n_users = await User.count()
|
||||
try {
|
||||
if (n_users === 0) {
|
||||
// the first registered user will be an active admin
|
||||
req.body.is_active = req.body.is_admin = true
|
||||
} else {
|
||||
req.body.is_active = false
|
||||
}
|
||||
const user = await User.create(req.body)
|
||||
try {
|
||||
mail.send([user.email, config.admin], 'register', { user, config })
|
||||
} catch (e) {
|
||||
return res.status(400).json(e)
|
||||
}
|
||||
const payload = { email: user.email }
|
||||
const token = jwt.sign(payload, config.secret)
|
||||
res.json({ user, token })
|
||||
} catch (e) {
|
||||
res.status(404).json(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userController
|
||||
5
server/api/db.js
Normal file
5
server/api/db.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const conf = require('./config.js')
|
||||
const db = new Sequelize(conf.db)
|
||||
// db.sync({force: true})
|
||||
module.exports = db
|
||||
86
server/api/index.js
Normal file
86
server/api/index.js
Normal file
@@ -0,0 +1,86 @@
|
||||
const express = require('express')
|
||||
const multer = require('multer')
|
||||
const { fillUser, isAuth, isAdmin } = require('./auth')
|
||||
const eventController = require('./controller/event')
|
||||
const exportController = require('./controller/export')
|
||||
const userController = require('./controller/user')
|
||||
const settingsController = require('./controller/settings')
|
||||
|
||||
// const botController = require('./controller/bot')
|
||||
|
||||
const storage = require('./storage')({
|
||||
destination: 'uploads/'
|
||||
})
|
||||
|
||||
const upload = multer({ storage })
|
||||
const api = express.Router()
|
||||
// login
|
||||
api.post('/login', userController.login)
|
||||
api.post('/user/recover', userController.forgotPassword)
|
||||
api.post('/user/check_recover_code', userController.checkRecoverCode)
|
||||
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
|
||||
|
||||
api
|
||||
.route('/user')
|
||||
// register
|
||||
.post(userController.register)
|
||||
// get current user
|
||||
.get(isAuth, userController.current)
|
||||
// update user (eg. confirm)
|
||||
.put(isAuth, isAdmin, userController.update)
|
||||
|
||||
// get all users
|
||||
api.get('/users', isAuth, isAdmin, userController.getAll)
|
||||
|
||||
// update a tag (modify color)
|
||||
api.put('/tag', isAuth, isAdmin, eventController.updateTag)
|
||||
|
||||
// update a place (modify address..)
|
||||
api.put('/place', isAuth, isAdmin, eventController.updatePlace)
|
||||
|
||||
api
|
||||
.route('/user/event')
|
||||
// add event
|
||||
.post(fillUser, upload.single('image'), userController.addEvent)
|
||||
// update event
|
||||
.put(isAuth, upload.single('image'), userController.updateEvent)
|
||||
|
||||
// remove event
|
||||
api.delete('/user/event/:id', isAuth, userController.delEvent)
|
||||
|
||||
// get tags/places
|
||||
api.get('/event/meta', eventController.getMeta)
|
||||
|
||||
// get unconfirmed events
|
||||
api.get('/event/unconfirmed', isAuth, isAdmin, eventController.getUnconfirmed)
|
||||
|
||||
// add event notification
|
||||
api.post('/event/notification', eventController.addNotification)
|
||||
api.delete('/event/notification/:code', eventController.delNotification)
|
||||
|
||||
api.get('/settings', settingsController.getAdminSettings)
|
||||
api.post('/settings', settingsController.setAdminSetting)
|
||||
|
||||
// get event
|
||||
api.get('/event/:event_id', eventController.get)
|
||||
|
||||
// confirm event
|
||||
api.get('/event/confirm/:event_id', isAuth, isAdmin, eventController.confirm)
|
||||
api.get(
|
||||
'/event/unconfirm/:event_id',
|
||||
isAuth,
|
||||
isAdmin,
|
||||
eventController.unconfirm
|
||||
)
|
||||
|
||||
// export events (rss/ics)
|
||||
api.get('/export/:type', exportController.export)
|
||||
|
||||
// get events in this range
|
||||
api.get('/event/:year/:month', eventController.getAll)
|
||||
|
||||
// mastodon oauth auth
|
||||
api.post('/user/getauthurl', isAuth, userController.getAuthURL)
|
||||
api.post('/user/code', isAuth, userController.code)
|
||||
|
||||
module.exports = api
|
||||
44
server/api/mail.js
Normal file
44
server/api/mail.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const Email = require('email-templates')
|
||||
const path = require('path')
|
||||
const config = require('./config')
|
||||
const moment = require('moment')
|
||||
moment.locale('it')
|
||||
|
||||
const mail = {
|
||||
send (addresses, template, locals) {
|
||||
const email = new Email({
|
||||
views: { root: path.join(__dirname, 'emails') },
|
||||
juice: true,
|
||||
juiceResources: {
|
||||
preserveImportant: true,
|
||||
webResources: {
|
||||
relativeTo: path.join(__dirname, 'emails')
|
||||
}
|
||||
},
|
||||
message: {
|
||||
from: `${config.title} <${config.smtp.auth.user}>`
|
||||
},
|
||||
send: true,
|
||||
i18n: {
|
||||
locales: ['en', 'es', 'it'],
|
||||
defaultLocale: config.locale
|
||||
},
|
||||
transport: config.smtp
|
||||
})
|
||||
return email.send({
|
||||
template,
|
||||
message: {
|
||||
to: addresses,
|
||||
bcc: config.admin
|
||||
},
|
||||
locals: {
|
||||
...locals,
|
||||
locale: config.locale,
|
||||
config,
|
||||
datetime: datetime => moment(datetime).format('ddd, D MMMM HH:mm')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = mail
|
||||
14
server/api/model.js
Normal file
14
server/api/model.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const User = require('./models/user')
|
||||
const { Event, Comment, Tag, Place, Notification, EventNotification } = require('./models/event')
|
||||
const Settings = require('./models/settings')
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Event,
|
||||
Comment,
|
||||
Tag,
|
||||
Place,
|
||||
Notification,
|
||||
EventNotification,
|
||||
Settings
|
||||
}
|
||||
72
server/api/models/event.js
Normal file
72
server/api/models/event.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const db = require('../db')
|
||||
const User = require('./user')
|
||||
|
||||
const Event = db.define('event', {
|
||||
title: Sequelize.STRING,
|
||||
description: Sequelize.TEXT,
|
||||
multidate: Sequelize.BOOLEAN,
|
||||
start_datetime: { type: Sequelize.DATE, index: true },
|
||||
end_datetime: { type: Sequelize.DATE, index: true },
|
||||
image_path: Sequelize.STRING,
|
||||
activitypub_id: { type: Sequelize.INTEGER, index: true },
|
||||
is_visible: Sequelize.BOOLEAN
|
||||
})
|
||||
|
||||
const Tag = db.define('tag', {
|
||||
tag: { type: Sequelize.STRING, index: true, unique: true, primaryKey: true },
|
||||
color: { type: Sequelize.STRING }
|
||||
})
|
||||
|
||||
const Comment = db.define('comment', {
|
||||
activitypub_id: { type: Sequelize.INTEGER, index: true },
|
||||
author: Sequelize.STRING,
|
||||
text: Sequelize.STRING
|
||||
})
|
||||
|
||||
const Notification = db.define('notification', {
|
||||
filters: Sequelize.JSON,
|
||||
email: Sequelize.STRING,
|
||||
remove_code: Sequelize.STRING,
|
||||
type: {
|
||||
type: Sequelize.ENUM,
|
||||
values: ['mail', 'admin_email', 'mastodon']
|
||||
}
|
||||
})
|
||||
|
||||
const Place = db.define('place', {
|
||||
name: { type: Sequelize.STRING, unique: true, index: true },
|
||||
address: { type: Sequelize.STRING }
|
||||
})
|
||||
|
||||
Comment.belongsTo(Event)
|
||||
Event.hasMany(Comment)
|
||||
|
||||
Event.belongsToMany(Tag, { through: 'tagEvent' })
|
||||
Tag.belongsToMany(Event, { through: 'tagEvent' })
|
||||
|
||||
const EventNotification = db.define('EventNotification', {
|
||||
status: {
|
||||
type: Sequelize.ENUM,
|
||||
values: ['new', 'sent', 'error'],
|
||||
defaultValue: 'new',
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
Event.belongsToMany(Notification, { through: EventNotification })
|
||||
Notification.belongsToMany(Event, { through: EventNotification })
|
||||
|
||||
Event.belongsTo(User)
|
||||
Event.belongsTo(Place)
|
||||
|
||||
User.hasMany(Event)
|
||||
Place.hasMany(Event)
|
||||
|
||||
// async function init() {
|
||||
// await Notification.findOrCreate({ where: { type: 'mastodon', filters: { is_visible: true } } })
|
||||
// await Notification.findOrCreate({ where: { type: 'admin_email', filters: { is_visible: false } } })
|
||||
// }
|
||||
|
||||
// init()
|
||||
module.exports = { Event, Comment, Tag, Place, Notification, EventNotification }
|
||||
37
server/api/models/index.js
Normal file
37
server/api/models/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Sequelize = require('sequelize');
|
||||
const basename = path.basename(__filename);
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const config = require(__dirname + '/../config/config.json')[env];
|
||||
const db = {};
|
||||
|
||||
let sequelize;
|
||||
if (config.use_env_variable) {
|
||||
sequelize = new Sequelize(process.env[config.use_env_variable], config);
|
||||
} else {
|
||||
sequelize = new Sequelize(config.database, config.username, config.password, config);
|
||||
}
|
||||
|
||||
fs
|
||||
.readdirSync(__dirname)
|
||||
.filter(file => {
|
||||
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
|
||||
})
|
||||
.forEach(file => {
|
||||
const model = sequelize['import'](path.join(__dirname, file));
|
||||
db[model.name] = model;
|
||||
});
|
||||
|
||||
Object.keys(db).forEach(modelName => {
|
||||
if (db[modelName].associate) {
|
||||
db[modelName].associate(db);
|
||||
}
|
||||
});
|
||||
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
9
server/api/models/settings.js
Normal file
9
server/api/models/settings.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const db = require('../db')
|
||||
const Sequelize = require('sequelize')
|
||||
|
||||
const Settings = db.define('settings', {
|
||||
key: { type: Sequelize.STRING, primaryKey: true, index: true },
|
||||
value: Sequelize.JSON
|
||||
})
|
||||
|
||||
module.exports = Settings
|
||||
34
server/api/models/user.js
Normal file
34
server/api/models/user.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const bcrypt = require('bcrypt')
|
||||
const db = require('../db')
|
||||
const Sequelize = require('sequelize')
|
||||
|
||||
const User = db.define('user', {
|
||||
email: {
|
||||
type: Sequelize.STRING,
|
||||
unique: { msg: 'Email already exists' },
|
||||
index: true,
|
||||
allowNull: false
|
||||
},
|
||||
description: Sequelize.TEXT,
|
||||
password: Sequelize.STRING,
|
||||
recover_code: Sequelize.STRING,
|
||||
is_admin: Sequelize.BOOLEAN,
|
||||
is_active: Sequelize.BOOLEAN,
|
||||
mastodon_auth: Sequelize.JSON
|
||||
})
|
||||
|
||||
User.prototype.comparePassword = async function (pwd) {
|
||||
if (!this.password) return false
|
||||
const ret = await bcrypt.compare(pwd, this.password)
|
||||
return ret
|
||||
}
|
||||
|
||||
User.beforeSave(async (user, options) => {
|
||||
if (user.changed('password')) {
|
||||
const salt = await bcrypt.genSalt(10)
|
||||
const hash = await bcrypt.hash(user.password, salt)
|
||||
user.password = hash
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = User
|
||||
62
server/api/storage.js
Normal file
62
server/api/storage.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
const mkdirp = require('mkdirp')
|
||||
const sharp = require('sharp')
|
||||
|
||||
function getDestination(req, file, cb) {
|
||||
cb(null, os.tmpdir())
|
||||
}
|
||||
|
||||
function DiskStorage(opts) {
|
||||
if (typeof opts.destination === 'string') {
|
||||
mkdirp.sync(opts.destination)
|
||||
this.getDestination = function ($0, $1, cb) { cb(null, opts.destination) }
|
||||
} else {
|
||||
this.getDestination = (opts.destination || getDestination)
|
||||
}
|
||||
}
|
||||
|
||||
DiskStorage.prototype._handleFile = function _handleFile(req, file, cb) {
|
||||
const that = this
|
||||
that.getDestination(req, file, function (err, destination) {
|
||||
if (err) return cb(err)
|
||||
|
||||
const filename = crypto.randomBytes(16).toString('hex') + '.jpg'
|
||||
const finalPath = path.join(destination, filename)
|
||||
const thumbPath = path.join(destination, 'thumb', filename)
|
||||
const outStream = fs.createWriteStream(finalPath)
|
||||
const thumbStream = fs.createWriteStream(thumbPath)
|
||||
const resizer = sharp().resize(800).jpeg({ quality: 80 })
|
||||
const thumbnailer = sharp().resize(400).jpeg({ quality: 60 })
|
||||
|
||||
file.stream.pipe(thumbnailer).pipe(thumbStream)
|
||||
thumbStream.on('error', e => console.log('thumbStream error ', e))
|
||||
|
||||
file.stream.pipe(resizer).pipe(outStream)
|
||||
outStream.on('error', cb)
|
||||
outStream.on('finish', function () {
|
||||
cb(null, {
|
||||
destination,
|
||||
filename,
|
||||
path: finalPath,
|
||||
size: outStream.bytesWritten
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
DiskStorage.prototype._removeFile = function _removeFile(req, file, cb) {
|
||||
let path = file.path
|
||||
|
||||
delete file.destination
|
||||
delete file.filename
|
||||
delete file.path
|
||||
|
||||
fs.unlink(path, cb)
|
||||
}
|
||||
|
||||
module.exports = function (opts) {
|
||||
return new DiskStorage(opts)
|
||||
}
|
||||
Reference in New Issue
Block a user