diff --git a/Dockerfile b/Dockerfile index 0c140f99..96dc6f9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,15 +4,17 @@ FROM node:10 WORKDIR /usr/src/app COPY package.json . +COPY pm2.json . # install backend dependencies -RUN yarn +RUN yarn --prod # copy source -COPY . . +COPY app app/ +COPY client client/ # install nodemon -RUN yarn global add nodemon +RUN yarn global add pm2 WORKDIR /usr/src/app/client @@ -26,4 +28,4 @@ WORKDIR /usr/src/app EXPOSE 12300 -CMD [ "yarn", "run", "serve" ] +CMD [ "pm2-runtime", "start", "pm2.json" ] diff --git a/app/api.js b/app/api.js index 0f8c2fe2..d88fe22a 100644 --- a/app/api.js +++ b/app/api.js @@ -4,16 +4,10 @@ const eventController = require('./controller/event') const exportController = require('./controller/export') const userController = require('./controller/user') // const botController = require('./controller/bot') - -const path = require('path') const multer = require('multer') -const crypto = require('crypto') const storage = require('./storage')({ - destination: 'uploads/', - filename: (req, file, cb) => { - cb(null, crypto.randomBytes(16).toString('hex') + path.extname(file.originalname)) - } + destination: 'uploads/' }) const upload = multer({ storage }) const api = express.Router() diff --git a/app/auth.js b/app/auth.js index 71396057..56afe236 100644 --- a/app/auth.js +++ b/app/auth.js @@ -1,26 +1,24 @@ const jwt = require('jsonwebtoken') const config = require('./config') const User = require('./models/user') +const { Op } = require('sequelize') const Auth = { async fillUser (req, res, next) { const token = req.body.token || req.params.token || req.headers['x-access-token'] - console.log('[AUTH] ', token) - if (!token) next() + if (!token) return next() jwt.verify(token, config.secret, async (err, decoded) => { - if (err) next() - req.user = await User.findOne({ where: { email: decoded.email, is_active: true } }) + if (err) return next() + req.user = await User.findOne({ where: { email: { [Op.eq]: decoded.email }, is_active: true } }) next() }) }, async isAuth (req, res, next) { const token = req.body.token || req.params.token || req.headers['x-access-token'] - console.log('[AUTH] ', 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 }) - console.log('DECODED TOKEN', decoded) - req.user = await User.findOne({ where: { email: decoded.email, is_active: true } }) + 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() }) diff --git a/app/config.js b/app/config.js index 355c9a94..a09c9615 100644 --- a/app/config.js +++ b/app/config.js @@ -20,9 +20,9 @@ if (process.env.NODE_ENV === 'production') { } module.exports = { - locale: 'en', + locale: 'it', - title: process.env.TITLE || 'Put here your site name', + title: process.env.TITLE || 'GANCIO', description: process.env.DESCRIPTION || 'A calendar for radical communities', baseurl: process.env.BASE_URL || 'http://localhost:8080', diff --git a/app/controller/event.js b/app/controller/event.js index 5a89d33e..33d196bb 100644 --- a/app/controller/event.js +++ b/app/controller/event.js @@ -1,14 +1,14 @@ -const { User, Event, Comment, Tag, Place, MailReminder } = require('../model') +const { User, Event, Comment, Tag, Place, Reminder } = require('../model') const moment = require('moment') -const Sequelize = require('sequelize') - +const { Op } = require('sequelize') +const lodash = require('lodash') 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: req.body.id } }) + let event = await Event.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } } }) if (!event) { - const comment = await Comment.findOne({ where: { activitypub_id: req.body.id }, include: 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) @@ -23,6 +23,26 @@ const eventController = { res.json({ tags, places }) }, + async getReminders (event) { + function match (event, filters) { + // matches if no filter specified + 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 reminders = await Reminder.findAll() + + // get reminder that matches with selected event + return reminders.filter(reminder => match(event, reminder.filters)) + }, + async updateTag (req, res) { const tag = await Tag.findByPk(req.body.tag) console.log(tag) @@ -68,8 +88,12 @@ const eventController = { }, async addReminder (req, res) { - await MailReminder.create(req.body.reminder) - res.json(200) + try { + await Reminder.create(req.body) + res.sendStatus(200) + } catch (e) { + res.sendStatus(404) + } }, async getAll (req, res) { const start = moment().year(req.params.year).month(req.params.month).startOf('month').subtract(1, 'week') @@ -77,9 +101,9 @@ const eventController = { const events = await Event.findAll({ where: { is_visible: true, - [Sequelize.Op.and]: [ - { start_datetime: { [Sequelize.Op.gte]: start } }, - { start_datetime: { [Sequelize.Op.lte]: end } } + [Op.and]: [ + { start_datetime: { [Op.gte]: start } }, + { start_datetime: { [Op.lte]: end } } ] }, order: [['start_datetime', 'ASC']], diff --git a/app/controller/export.js b/app/controller/export.js index 6ad3c5c7..dba94cf7 100644 --- a/app/controller/export.js +++ b/app/controller/export.js @@ -1,5 +1,5 @@ const { Event, Comment, Tag, Place } = require('../model') -const Sequelize = require('sequelize') +const { Op } = require('sequelize') const config = require('../config') const moment = require('moment') const ics = require('ics') @@ -21,7 +21,7 @@ const exportController = { } const events = await Event.findAll({ order: [['start_datetime', 'ASC']], - where: { start_datetime: { [Sequelize.Op.gte]: yesterday } }, + where: { start_datetime: { [Op.gte]: yesterday } }, include: [Comment, { model: Tag, where: whereTag diff --git a/app/controller/user.js b/app/controller/user.js index 7e5b7617..9a84ba5d 100644 --- a/app/controller/user.js +++ b/app/controller/user.js @@ -3,14 +3,16 @@ const Mastodon = require('mastodon-api') const User = require('../models/user') const { Event, Tag, Place } = require('../models/event') +const eventController = require('./event') const config = require('../config') const mail = require('../mail') const bot = require('./bot') +const { Op } = require('sequelize') const userController = { async login (req, res) { // find the user - const user = await User.findOne({ where: { email: req.body.email } }) + 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) { @@ -82,6 +84,7 @@ const userController = { } catch (e) { console.log(e) } + let event = await Event.create(eventDetails) await event.setPlace(place) @@ -89,7 +92,7 @@ const userController = { console.log(body.tags) if (body.tags) { await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true }) - const tags = await Tag.findAll({ where: { tag: body.tags } }) + const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } }) await event.addTags(tags) } if (req.user) await req.user.addEvent(event) @@ -102,7 +105,9 @@ const userController = { event.save() } - mail.send(config.admin, 'event', { event }) + // insert reminder + const reminders = await eventController.getReminders(event) + await event.setReminders(reminders) return res.json(event) }, @@ -121,7 +126,7 @@ const userController = { await event.update(body) let place try { - place = await Place.findOrCreate({ where: { name: body.place_name }, + place = await Place.findOrCreate({ where: { name: { [Op.eq]: body.place_name } }, defaults: { address: body.place_address } }) .spread((place, created) => place) } catch (e) { @@ -132,7 +137,7 @@ const userController = { console.log(body.tags) if (body.tags) { await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true }) - const tags = await Tag.findAll({ where: { tag: body.tags } }) + const tags = await Tag.findAll({ where: { tag: { [Op.eq]: body.tags } } }) await event.addTags(tags) } const newEvent = await Event.findByPk(event.id, { include: [User, Tag, Place] }) diff --git a/app/cron.js b/app/cron.js new file mode 100644 index 00000000..4e184390 --- /dev/null +++ b/app/cron.js @@ -0,0 +1,27 @@ +const mail = require('./mail') +const { Event, Reminder, EventReminder, User, Place, Tag } = require('./model') + +async function loop () { + console.log('nel loop') + // get all event reminder in queue + const eventReminders = await EventReminder.findAll() + const promises = eventReminders.map(async e => { + const event = await Event.findByPk(e.eventId, { include: [User, Place, Tag] }) + console.log('EVENT ') + console.log(event) + if (!event.place) return + const reminder = await Reminder.findByPk(e.reminderId) + try { + await mail.send(reminder.email, 'event', { event }) + } catch (e) { + console.log('DENTRO CATCH!', e) + return false + } + return e.destroy() + }) + + return Promise.all(promises) +} + +setInterval(loop, 20000) +loop() diff --git a/app/emails/event/html.pug b/app/emails/event/html.pug new file mode 100644 index 00000000..853ad236 --- /dev/null +++ b/app/emails/event/html.pug @@ -0,0 +1,10 @@ +h3 #{event.title} +p Dove: #{event.place.name} - #{event.place.address} +p Quando: #{datetime(event.start_datetime)} +br + +p #{event.description} + +#{config.baseurl}/event/#{event.id} +hr +#{config.title} - #{config.description} \ No newline at end of file diff --git a/app/emails/event/subject.pug b/app/emails/event/subject.pug new file mode 100644 index 00000000..fd159324 --- /dev/null +++ b/app/emails/event/subject.pug @@ -0,0 +1 @@ += `[${config.title}] ${event.title} @${event.place.name} ${datetime(event.start_datetime)}` diff --git a/emails/mail.css b/app/emails/mail.css similarity index 100% rename from emails/mail.css rename to app/emails/mail.css diff --git a/emails/register/html.pug b/app/emails/register/html.pug similarity index 100% rename from emails/register/html.pug rename to app/emails/register/html.pug diff --git a/emails/register/subject.pug b/app/emails/register/subject.pug similarity index 100% rename from emails/register/subject.pug rename to app/emails/register/subject.pug diff --git a/locales/en.json b/app/locales/en.json similarity index 100% rename from locales/en.json rename to app/locales/en.json diff --git a/locales/es.json b/app/locales/es.json similarity index 100% rename from locales/es.json rename to app/locales/es.json diff --git a/locales/it.json b/app/locales/it.json similarity index 100% rename from locales/it.json rename to app/locales/it.json diff --git a/locales/zh.json b/app/locales/zh.json similarity index 100% rename from locales/zh.json rename to app/locales/zh.json diff --git a/app/mail.js b/app/mail.js index c02dc611..911eba02 100644 --- a/app/mail.js +++ b/app/mail.js @@ -1,16 +1,19 @@ 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) { locals.locale = config.locale const email = new Email({ + views: { root: path.join(__dirname, 'emails') }, juice: true, juiceResources: { preserveImportant: true, webResources: { - relativeTo: path.join(__dirname, '..', 'emails') + relativeTo: path.join(__dirname, 'emails') } }, message: { @@ -26,7 +29,7 @@ const mail = { to: addresses, bcc: config.admin }, - locals + locals: { ...locals, config, datetime: datetime => moment(datetime).format('ddd, D MMMM HH:mm') } }) } } diff --git a/app/model.js b/app/model.js index 8948a26b..2ff5d4d3 100644 --- a/app/model.js +++ b/app/model.js @@ -1,4 +1,4 @@ const User = require('./models/user') -const { Event, Comment, Tag, Place, MailReminder } = require('./models/event') +const { Event, Comment, Tag, Place, Reminder, EventReminder } = require('./models/event') -module.exports = { User, Event, Comment, Tag, Place, MailReminder } +module.exports = { User, Event, Comment, Tag, Place, Reminder, EventReminder } diff --git a/app/models/event.js b/app/models/event.js index ccb45d25..0928c10e 100644 --- a/app/models/event.js +++ b/app/models/event.js @@ -24,10 +24,10 @@ const Comment = db.define('comment', { text: Sequelize.STRING }) -const MailReminder = db.define('reminder', { +const Reminder = db.define('reminder', { filters: Sequelize.JSON, - mail: Sequelize.STRING, - send_on_add: Sequelize.BOOLEAN, + email: Sequelize.STRING, + notify_on_add: Sequelize.BOOLEAN, send_reminder: Sequelize.BOOLEAN }) @@ -42,10 +42,14 @@ Event.hasMany(Comment) Event.belongsToMany(Tag, { through: 'tagEvent' }) Tag.belongsToMany(Event, { through: 'tagEvent' }) +const EventReminder = db.define('EventReminder') +Event.belongsToMany(Reminder, { through: EventReminder }) +Reminder.belongsToMany(Event, { through: EventReminder }) + Event.belongsTo(User) Event.belongsTo(Place) User.hasMany(Event) Place.hasMany(Event) -module.exports = { Event, Comment, Tag, Place, MailReminder } +module.exports = { Event, Comment, Tag, Place, Reminder, EventReminder } diff --git a/server.js b/app/server.js similarity index 55% rename from server.js rename to app/server.js index e8817404..e7006ffb 100644 --- a/server.js +++ b/app/server.js @@ -1,21 +1,22 @@ const express = require('express') const app = express() const bodyParser = require('body-parser') -const api = require('./app/api') +const api = require('./api') const cors = require('cors') const path = require('path') const port = process.env.PORT || 9000 +app.set('views', path.join(__dirname, 'views')) app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.json()) app.use(cors()) app.use('/static', express.static(path.join(__dirname, 'uploads'))) app.use('/uploads', express.static('uploads')) app.use('/api', api) -app.use('/', express.static(path.join(__dirname, 'client', 'dist'))) -app.use('/css', express.static(path.join(__dirname, 'client', 'dist', 'css'))) -app.use('/js', express.static(path.join(__dirname, 'client', 'dist', 'js'))) -app.use('*', express.static(path.join(__dirname, 'client', 'dist', 'index.html'))) +app.use('/', express.static(path.join(__dirname, '..', 'client', 'dist'))) +app.use('/css', express.static(path.join(__dirname, '..', 'client', 'dist', 'css'))) +app.use('/js', express.static(path.join(__dirname, '..', 'client', 'dist', 'js'))) +app.use('*', express.static(path.join(__dirname, '..', 'client', 'dist', 'index.html'))) app.listen(port) console.log('Magic happens at http://localhost:' + port) diff --git a/app/storage.js b/app/storage.js new file mode 100644 index 00000000..4c81ce1d --- /dev/null +++ b/app/storage.js @@ -0,0 +1,56 @@ +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) { + var that = this + that.getDestination(req, file, function (err, destination) { + if (err) return cb(err) + + const filename = crypto.randomBytes(16).toString('hex') + '.webp' + const finalPath = path.join(destination, filename) + const outStream = fs.createWriteStream(finalPath) + const resizer = sharp().resize(800).webp() + + 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) { + var path = file.path + + delete file.destination + delete file.filename + delete file.path + + fs.unlink(path, cb) +} + +module.exports = function (opts) { + return new DiskStorage(opts) +} diff --git a/views/feed/rss.pug b/app/views/feed/rss.pug similarity index 100% rename from views/feed/rss.pug rename to app/views/feed/rss.pug diff --git a/client/.eslintrc.js b/client/.eslintrc.js index b6351b05..2f16b25f 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,3 +1,3 @@ module.exports = { - "extends": "standard" -}; + 'extends': 'standard' +} diff --git a/client/src/components/Admin.vue b/client/src/components/Admin.vue index 0b246a82..87317905 100644 --- a/client/src/components/Admin.vue +++ b/client/src/components/Admin.vue @@ -37,7 +37,7 @@ v-icon(name='calendar') span.ml-1 {{$t('Events')}} p {{$t('event_confirm_explanation')}} - el-table(:data='paginatedEvents' small primary-key='id') + el-table(:data='paginatedEvents' small primary-key='id' v-loading='loading') el-table-column(:label='$t("Name")') template(slot-scope='data') {{data.row.title}} el-table-column(:label='$t("Where")') @@ -76,6 +76,7 @@