first commit backend

This commit is contained in:
lesion 2019-02-26 00:02:42 +01:00
commit 887157f2a9
27 changed files with 819 additions and 0 deletions

5
.editorconfig Normal file
View file

@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

21
.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

31
app/TODO.md Normal file
View file

@ -0,0 +1,31 @@
# features importanti
- filtri (posto, tag, full text)
- routing interface permission
- admin:
- vedere utenti
- bloccare utenti
- approvare utenti
- disapprovare utenti
- scrape facebook
- output
- rss
- ics
- mail
- mastodon.
- embed
gancio.cisti.org
----
# mastodon
- autodifesa
- mastodon social
#stakkastakka
cosa parlare
#cavo
-------
Ci sentiamo spesso rispondere con un'alzata di spalle e un "ma su facebook ci sono tutti",
e' vero, anche
Per noi facebook e'

50
app/api.js Normal file
View file

@ -0,0 +1,50 @@
const express = require('express')
const { isAuth, isAdmin } = require('./auth')
const eventController = require('./controller/event')
const exportController = require('./controller/export')
// const botController = require('./controller/bot')
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })
const api = express.Router()
// USER API
const userController = require('./controller/user')
api.route('/login')
.post(userController.login)
api.route('/user')
.post(userController.register)
.get(isAuth, userController.current)
.put(isAuth, isAdmin, userController.update)
api.get('/users', isAuth, isAdmin, userController.getAll)
api.put('/tag', isAuth, isAdmin, eventController.updateTag)
api.route('/user/event')
.post(isAuth, upload.single('image'), userController.addEvent)
.get(isAuth, userController.getMyEvents)
.put(isAuth, upload.single('image'), userController.updateEvent)
api.route('/user/event/:id')
.delete(isAuth, userController.delEvent)
api.route('/event/:event_id')
.get(eventController.get)
api.route('/event/meta')
.get(eventController.getMeta)
api.get('/export/feed', exportController.feed)
api.get('/export/ics', exportController.ics)
api.route('/event/:year/:month')
.get(eventController.getAll)
api.post('/user/getauthurl', isAuth, userController.getAuthURL)
api.post('/user/code', isAuth, userController.code)
module.exports = api

23
app/auth.js Normal file
View file

@ -0,0 +1,23 @@
const jwt = require('jsonwebtoken')
const config = require('./config')
const User = require('./models/user')
const Auth = {
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}})
next()
})
},
async isAdmin (req, res, next) {
if (req.user.is_admin) return next()
return res.status(403).send({ message: 'Admin needed' })
}
}
module.exports = Auth

2
app/config.js Normal file
View file

@ -0,0 +1,2 @@
const env = process.env.NODE_ENV
module.exports = require('../config/config.' + env + '.js')

74
app/controller/bot.js Normal file
View file

@ -0,0 +1,74 @@
const jwt = require('jsonwebtoken')
const { User, Event, Comment, Tag } = require('../model')
const config = require('../config')
const mail = require('../mail')
const Mastodon = require('mastodon-api')
const Sequelize = require('sequelize')
const Op = Sequelize.Op
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.instance}/api/v1/` })
console.log(bot)
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) {
const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.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})
},
post(user, event) {
const { bot } = botController.bots.filter(b => b.email === user.email)[0]
const status = `${event.title} @ ${event.place.name} ${moment(event.start_datetime).format('ddd, D MMMM HH:mm')} -
${event.description} - ${event.tags.map(t => '#'+t.tag).join(' ')} ${config.baseurl}/event/${event.id}`
return bot.post('/statuses', { status, visibility: 'private' })
},
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)
return
// 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

70
app/controller/event.js Normal file
View file

@ -0,0 +1,70 @@
const jwt = require('jsonwebtoken')
const { User, Event, Comment, Tag, Place } = require('../model')
const config = require('../config')
const mail = require('../mail')
const moment = require('moment')
const Sequelize = require('sequelize')
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}})
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)
res.json(comment)
},
// async boost (req, res) {
// const event = await Event.findById(req.body.id)
// req.user.addBoost(event)
// res.status(200)
// },
async getMeta(req, res) {
const places = await Place.findAll()
const tags = await Tag.findAll()
res.json({tags, places})
},
async updateTag (req, res) {
const tag = await Tag.findByPk(req.body.tag)
console.log(tag)
if (tag) {
res.json(await tag.update(req.body))
} else {
res.send(404)
}
},
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 getAll (req, res) {
const start = moment().year(req.params.year).month(req.params.month).startOf('month').subtract(1, 'week')
const end = moment().year(req.params.year).month(req.params.month).endOf('month').add(1, 'week')
console.log('start', start)
console.log('end', end)
const events = await Event.findAll({
where: {
[Sequelize.Op.and]: [
{ start_datetime: { [Sequelize.Op.gte]: start } },
{ start_datetime: { [Sequelize.Op.lte]: end } }
]
},
order: [['createdAt', 'ASC']],
include: [User, Comment, Tag, Place]
})
res.json(events)
},
}
module.exports = eventController

45
app/controller/export.js Normal file
View file

@ -0,0 +1,45 @@
const jwt = require('jsonwebtoken')
const { User, Event, Comment, Tag, Place } = require('../model')
const config = require('../config')
const mail = require('../mail')
const moment = require('moment')
const Sequelize = require('sequelize')
const ics = require('ics')
const exportController = {
async getAll (req, res) {
const events = await Event.findAll({
where: {
[Sequelize.Op.and]: [
{ start_datetime: { [Sequelize.Op.gte]: start } },
{ start_datetime: { [Sequelize.Op.lte]: end } }
]
},
order: [['createdAt', 'DESC']],
include: [User, Comment, Tag, Place]
})
res.json(events)
},
async feed (req, res) {
const events = await Event.findAll({include: [Comment, Tag, Place]})
res.type('application/rss+xml; charset=UTF-8')
res.render('feed/rss.pug', {events, config, moment})
},
async ics (req, res) {
const events = await Event.findAll({include: [Comment, Tag, Place]})
console.log(events)
const eventsMap = events.map(e => ({
start: [2019, 2, 2],
end: [2019, 2, 3],
title: e.title,
description: e.description,
location: e.place.name
}))
res.type('text/calendar; charset=UTF-8')
const { error, value } = ics.createEvents(eventsMap)
console.log(value)
res.send(value)
}
}
module.exports = exportController

198
app/controller/user.js Normal file
View file

@ -0,0 +1,198 @@
const jwt = require('jsonwebtoken')
const Mastodon = require('mastodon-api')
const User = require('../models/user')
const { Event, Tag, Place } = require('../models/event')
const config = require('../config')
const mail = require('../mail')
const bot = require('./bot')
const userController = {
async login (req, res) {
// find the user
const user = await User.findOne({where: { email: 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) {
//check if event is mine
const event = await Event.findByPk(req.params.id)
if (event && (req.user.is_admin || req.user.id === event.userId))
{
await event.destroy()
res.sendStatus(200)
} else {
res.sendStatus(404)
}
},
async addEvent (req, res, next) {
const body = req.body
const eventDetails = {
title: body.title,
description: body.description,
multidate: body.multidate,
start_datetime: body.start_datetime,
end_datetime: body.end_datetime
}
if (req.file) {
eventDetails.image_path = req.file.path
}
//create place
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(e)
}
let event = await Event.create(eventDetails)
await event.setPlace(place)
// create/assign tags
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 }})
await event.addTags(tags)
}
await req.user.addEvent(event)
event = await Event.findByPk(event.id, {include: [User, Tag, Place]})
// check if bot exists
if (req.user.mastodon_auth) {
const post = await bot.post(req.user, event)
}
return res.json(event)
},
async updateEvent (req, res) {
const body = req.body
const event = await Event.findByPk(body.id)
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('catch', e)
}
await event.setPlace(place)
await event.setTags([])
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 }})
await event.addTags(tags)
}
const newEvent = await Event.findByPk(event.id, {include: [User, Tag, Place]})
// check if bot exists
if (req.user.mastodon_auth) {
const post = await bot.post(req.user, newEvent)
}
return res.json(newEvent)
},
async getMyEvents (req, res) {
const events = await req.user.getEvents()
res.json(events)
},
async getAuthURL (req, res) {
const instance = req.body.instance
const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`, 'eventi', 'read write', `${config.baseurl}/settings`)
const url = await Mastodon.getAuthorizationUrl(client_id, client_secret, `https://${instance}`, 'read write', `${config.baseurl}/settings`)
console.log(req.user)
req.user.instance = instance
req.user.mastodon_auth = { client_id, client_secret }
await req.user.save()
res.json(url)
},
async code (req, res) {
const code = req.body.code
const { client_id, client_secret } = req.user.mastodon_auth
const instance = req.user.instance
try {
const token = await Mastodon.getAccessToken(client_id, client_secret, code, `https://${instance}`, '${config.baseurl}/settings')
const mastodon_auth = { client_id, client_secret, access_token: token}
req.user.mastodon_auth = mastodon_auth
await req.user.save()
await botController.add(token)
res.json(req.user)
} catch (e) {
res.json(e)
}
},
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) {
await user.update(req.body)
res.json(user)
} else {
res.send(400)
}
},
async register (req, res) {
try {
req.body.is_active = false
const user = await User.create(req.body)
try {
mail.send(user.email, 'register', { user })
} catch (e) {
console.log(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

10
app/db.js Normal file
View file

@ -0,0 +1,10 @@
const Sequelize = require('sequelize')
const env = process.env.NODE_ENV || 'development'
const conf = require('../config/config.' + env + '.js')
const db = new Sequelize(conf.db)
db.sync({ force: true })
// db.sync()
module.exports = db

34
app/mail.js Normal file
View file

@ -0,0 +1,34 @@
const Email = require('email-templates')
const path = require('path')
const config = require('./config');
const mail = {
send (addresses, template, locals) {
locals.locale = config.locale
const email = new Email({
juice: true,
juiceResources: {
preserveImportant: true,
webResources: {
relativeTo: path.join(__dirname, '..', 'emails')
}
},
message: {
from: 'Gancio <eventi@cisti.org>'
},
send: true,
i18n: {},
transport: config.smtp
})
return email.send({
template,
message: {
to: addresses,
bcc: config.admin
},
locals
})
}
}
module.exports = mail

4
app/model.js Normal file
View file

@ -0,0 +1,4 @@
const User = require('./models/user')
const { Event, Comment, Tag, Place } = require('./models/event')
module.exports = { User, Event, Comment, Tag, Place }

52
app/models/event.js Normal file
View file

@ -0,0 +1,52 @@
const db = require('../db')
const Sequelize = require('sequelize')
const User = require('./user')
const Event = db.define('event', {
title: Sequelize.STRING,
description: Sequelize.STRING,
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 },
})
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 MailSubscription = db.define('subscription' , {
filters: Sequelize.JSON,
mail: Sequelize.TEXT,
send_on_add: Sequelize.BOOLEAN,
send_reminder: Sequelize.INTEGER,
})
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'})
Event.belongsToMany(User, {through: 'boost'})
Event.belongsTo(User)
Event.belongsTo(Place)
User.hasMany(Event)
Place.hasMany(Event)
User.belongsToMany(User, {through: 'userFollower', as: 'follower'})
module.exports = { Event, Comment, Tag, Place }

33
app/models/user.js Normal file
View file

@ -0,0 +1,33 @@
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,
is_admin: Sequelize.BOOLEAN,
is_active: Sequelize.BOOLEAN,
instance: Sequelize.STRING,
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

View file

@ -0,0 +1,35 @@
const path = require('path')
module.exports = {
// environment
env: 'development',
locale: 'it',
title: 'Gancio',
description: 'Un calendario dei movimenti piemontesi',
// base url
baseurl: 'http://localhost:8080',
apiurl: 'http://localhost:9000/api',
// db configuration
db: {
'storage': path.join(__dirname, '/../db.sqlite'),
'dialect': 'sqlite'
},
admin: 'lesion@autistici.org',
// email configuration
smtp: {
host: 'mail.example.com',
secure: true,
auth: {
user: 'user@example.com',
pass: 'password'
}
},
// jwt secret
secret: 'nonosecretsuper'
}

View file

@ -0,0 +1,30 @@
module.exports = {
// environment
env: 'production',
locale: 'en',
title: 'Put here your site name',
description: 'A calendar for radical communities',
// base url
baseurl: 'https://example.com',
apiurl: 'https://example.com/api',
// db configuration
db: {
},
admin: 'admin@example.com',
// email configuration
smtp: {
host: 'mail.example.com',
secure: true,
auth: {
user: 'admin@example.com',
pass: ''
}
},
// jwt secret
secret: 'randomstringhere'
}

8
emails/mail.css Normal file
View file

@ -0,0 +1,8 @@
table {
width: 100%;
border-collapse: collapse;
}
table, th, td {
border: 1px solid #555;
}

6
emails/register/html.pug Normal file
View file

@ -0,0 +1,6 @@
h4 Gancio
p= t('registration_email')
small --
small https://cisti.org

View file

@ -0,0 +1 @@
= `[Gancio] Richiesta registrazione`

3
locales/en.json Normal file
View file

@ -0,0 +1,3 @@
{
"registration_email": "Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere."
}

3
locales/es.json Normal file
View file

@ -0,0 +1,3 @@
{
"registration_email": "registration_email"
}

3
locales/it.json Normal file
View file

@ -0,0 +1,3 @@
{
"registration_email": "Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere."
}

3
locales/zh.json Normal file
View file

@ -0,0 +1,3 @@
{
"registration_email": "registration_email"
}

33
package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "gancio",
"main": "server.js",
"scripts": {
"serve": "NODE_ENV=production PORT=9000 nodemon server.js",
"dev": "NODE_ENV=development PORT=9000 nodemon server.js"
},
"dependencies": {
"bcrypt": "^3.0.2",
"body-parser": "^1.15.0",
"cors": "^2.8.4",
"email-templates": "^5.0.2",
"express": "^4.13.4",
"ics": "^2.13.1",
"jsonwebtoken": "^5.7.0",
"mastodon-api": "^1.3.0",
"mongoose": "^5.2.17",
"morgan": "^1.7.0",
"multer": "^1.4.1",
"mysql2": "^1.6.4",
"pug": "^2.0.3",
"sequelize": "^4.41.0",
"sqlite3": "^4.0.3"
},
"devDependencies": {
"eslint": "^5.8.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0"
}
}

19
server.js Normal file
View file

@ -0,0 +1,19 @@
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const api = require('./app/api')
const cors = require('cors')
const path = require('path')
const db = require('./app/db')
const port = process.env.PORT || 8080
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.use('/static', express.static(path.join(__dirname, 'uploads')))
app.use('/uploads', express.static('uploads'))
app.use('/', express.static(path.join(__dirname, 'client', 'dist')))
app.use(cors())
app.use('/api', api)
app.listen(port)
console.log('Magic happens at http://localhost:' + port)

23
views/feed/rss.pug Normal file
View file

@ -0,0 +1,23 @@
doctype xml
rss(version='2.0', xmlns:atom='<a href="http://www.w3.org/2005/Atom" rel="nofollow">http://www.w3.org/2005/Atom</a>')
channel
title #{config.title}
link <a href="#{config.baseurl}" rel="nofollow">#{config.baseurl}</a>
atom:link(href='<a href="#{config.apiurl}/export/feed/rss" rel="nofollow">#{config.apiurl}/export/feed/rss</a>', rel='self', type='application/rss+xml')
description #{config.description}
language #{config.locale}
//- if events.length
lastBuildDate= new Date(posts[0].publishedAt).toUTCString()
each event in events
item
title= event.title
link <a href=#{config.baseurl}/event/#{event.id}" rel="nofollow">#{config.baseurl}/event/#{event.id}</a>
description
| <![CDATA[
| <h4>#{event.title}</h4>
| <strong>#{event.place.name} - #{event.place.address}</strong>
| #{moment(event.start_datetime).format("ddd, D MMMM HH:mm")}<br/>
| !{event.description}
| ]]>
pubDate= new Date(event.start_datetime).toUTCString()
guid(isPermaLink='false') <a href="#{config.baseurl}/event/#{event.id}" rel="nofollow">#{config.baseurl}/event/#{event.id}</a>