diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..5d126348
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+# editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 00000000..6c7dde5c
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,28 @@
+module.exports = {
+ root: true,
+ env: {
+ browser: true,
+ node: true
+ },
+ parserOptions: {
+ parser: 'babel-eslint'
+ },
+ extends: [
+ '@nuxtjs',
+ // 'plugin:nuxt/recommended',
+ // 'plugin:prettier/recommended',
+ // 'prettier',
+ // 'prettier/vue'
+ ],
+ plugins: [
+ 'prettier'
+ ],
+ // add your custom rules here
+ rules: {
+ 'nuxt/no-cjs-in-config': 'off',
+ 'camelcase': 'off',
+ 'no-console': 'off',
+ 'arrow-parens': 'off',
+ 'import/order': 'off'
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..b9a6eb25
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,89 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Gancio production configuration
+config.js
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+uploads
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# next.js build output
+.next
+
+# nuxt.js build output
+.nuxt
+
+# Nuxt generate
+dist
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless
+
+# IDE
+.idea
+
+# Service worker
+sw.*
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..b2095be8
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,4 @@
+{
+ "semi": false,
+ "singleQuote": true
+}
diff --git a/.sequelizerc b/.sequelizerc
new file mode 100644
index 00000000..31b6598f
--- /dev/null
+++ b/.sequelizerc
@@ -0,0 +1,7 @@
+const path = require('path')
+
+module.exports = {
+ 'config': path.resolve('config.js'),
+ 'migrations-path': path.resolve('server', 'migrations'),
+ 'models-path': path.resolve('server', 'api', 'models')
+}
diff --git a/.vscode/vscode-kanban.json b/.vscode/vscode-kanban.json
new file mode 100644
index 00000000..a487fe65
--- /dev/null
+++ b/.vscode/vscode-kanban.json
@@ -0,0 +1,331 @@
+{
+ "done": [
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:47:35.777Z",
+ "id": "5",
+ "prio": 3,
+ "references": [],
+ "title": "server side auth"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:50:00.973Z",
+ "description": {
+ "content": "- export page ok\n- usare un'altra api per retrieve di eventi (perche' devo mostrarli tutti, non solo quelli del mese corrente)\n- non devo fare il load degli eventi nel nuxtServerInit (o dentro il layout normale oppure nelle pagine, tipo nella home)",
+ "mime": "text/markdown"
+ },
+ "details": {
+ "content": "- export page ok\n- usare un'altra api per retrieve di eventi (perche' devo mostrarli tutti, non solo quelli del mese corrente)\n- non devo fare il load degli eventi nel nuxtServerInit (o dentro il layout normale oppure nelle pagine, tipo nella home)\n",
+ "mime": "text/markdown"
+ },
+ "id": "7",
+ "prio": 1,
+ "references": [],
+ "title": "export page",
+ "type": "bug"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:55:59.993Z",
+ "id": "10",
+ "prio": 1,
+ "references": [],
+ "title": "gestione errori form aggiungi evento",
+ "type": "bug"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:56:46.263Z",
+ "id": "11",
+ "prio": 1,
+ "references": [],
+ "title": "get comments / media from mastodon"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:46:46.332Z",
+ "id": "3",
+ "prio": 0,
+ "references": [],
+ "title": "export lista"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-04-30T12:46:42.208Z",
+ "id": "16",
+ "references": [],
+ "title": "riesco a rimuovere bootstrap-vue e usare solo element ?"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-04-28T09:25:50.701Z",
+ "id": "13",
+ "references": [],
+ "title": "test altra visualizzazione"
+ }
+ ],
+ "in-progress": [
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-04-30T22:00:29.237Z",
+ "id": "17",
+ "references": [],
+ "title": "porcoddio la config arriva anche al client ovviamente, devo separare!"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-04-30T12:31:20.056Z",
+ "id": "15",
+ "references": [],
+ "title": "test deploy"
+ }
+ ],
+ "testing": [],
+ "todo": [
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:50:55.458Z",
+ "id": "8",
+ "prio": 1,
+ "references": [],
+ "title": "rivedere ux / messaggi utente",
+ "type": "bug"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:45:27.613Z",
+ "description": {
+ "content": "probabilmente devo far diventare il campo senza timezone",
+ "mime": "text/markdown"
+ },
+ "id": "2",
+ "prio": 0,
+ "references": [],
+ "title": "check date timezone",
+ "type": "bug"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:48:54.407Z",
+ "description": {
+ "content": "probabilmente lato client dovrei aggiungere una classe css al body per capire se js e' attivo o meno e poi lavorare di css",
+ "mime": "text/markdown"
+ },
+ "id": "6",
+ "prio": 0,
+ "references": [],
+ "title": "risolvere le modali quando il js e' disabilitato",
+ "type": "bug"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-06-01T21:00:22.155Z",
+ "id": "28",
+ "references": [],
+ "title": "activitypub stream"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-05-27T15:42:35.467Z",
+ "id": "21",
+ "references": [],
+ "title": "all'admin deve mostrare un badge se ci sono pending operation"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-05-27T15:16:22.839Z",
+ "id": "20",
+ "references": [],
+ "title": "check password reset"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-06-01T21:23:46.941Z",
+ "id": "30",
+ "references": [],
+ "title": "choose listening port"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-05-27T15:45:39.093Z",
+ "id": "23",
+ "references": [],
+ "title": "colori te prego!"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-05-27T20:42:22.581Z",
+ "id": "24",
+ "references": [],
+ "title": "copy to clipboard"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-05-29T13:08:20.887Z",
+ "id": "25",
+ "references": [],
+ "title": "creazione script di backup"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-06-01T21:00:07.431Z",
+ "id": "27",
+ "references": [],
+ "title": "eventi ricorrenti"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-05-27T15:45:18.012Z",
+ "id": "22",
+ "references": [],
+ "title": "filtri per luogo!"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-04-29T10:01:01.632Z",
+ "id": "14",
+ "references": [],
+ "title": "gestione errori quando non c'e' un evento"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-05-02T16:15:07.106Z",
+ "id": "19",
+ "references": [],
+ "title": "modifica eventi multigiorno"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-05-02T11:50:28.476Z",
+ "id": "18",
+ "references": [],
+ "title": "notifiche email / iscrizione / evento da confermare"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:44:56.705Z",
+ "id": "1",
+ "references": [],
+ "title": "popup sul calendario"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-04-27T19:44:33.769Z",
+ "id": "12",
+ "references": [],
+ "title": "rifare il calendario o solo il popup"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-06-01T21:15:42.190Z",
+ "id": "29",
+ "references": [],
+ "title": "settings di istanza (default filter, eg, eventi ricorrenti)"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:47:10.704Z",
+ "id": "4",
+ "prio": 0,
+ "references": [],
+ "title": "traduzione in inglese"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-05-29T13:10:04.463Z",
+ "id": "26",
+ "references": [],
+ "title": "v-calendar colori e eventi multidays..."
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "category": "feature",
+ "creation_time": "2019-04-23T19:51:05.917Z",
+ "id": "9",
+ "prio": -1,
+ "references": [],
+ "title": "documentare sorgenti",
+ "type": "bug"
+ },
+ {
+ "assignedTo": {
+ "name": "lesion"
+ },
+ "creation_time": "2019-06-05T20:39:59.287Z",
+ "id": "31",
+ "references": [],
+ "title": "scroll h su mobile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..8c8b463f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+## gancio
+### event manager for radical communities
+
+> :warning: Gancio is under heavy development,
+> if something is not working as expected, it's expected :D
+
+
+## Install
+You will need `npm` or `yarn` installed in your system.
+
+
+``` bash
+# clone this repo
+git clone https://git.lattuga.net/cisti/gancio.git
+cd gancio
+
+# install dependencies
+yarn install
+
+# edit configuration
+cp config.example.js config.js
+
+# - migrate/create test sqlite db
+yarn migrate:dev
+
+# testing with sqlite db
+yarn dev
+
+# - migrate/create production db
+yarn migrate
+
+# build for production and launch server
+yarn build
+yarn start
+
+```
+
+##### nginx setup
+https://nuxtjs.org/faq/nginx-proxy
+
+For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org).
+
+
+## Hacking
+
+``` bash
+$ yarn dev
+
+```
\ No newline at end of file
diff --git a/assets/README.md b/assets/README.md
new file mode 100644
index 00000000..34766f93
--- /dev/null
+++ b/assets/README.md
@@ -0,0 +1,7 @@
+# ASSETS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
diff --git a/assets/style.less b/assets/style.less
new file mode 100644
index 00000000..09f8e442
--- /dev/null
+++ b/assets/style.less
@@ -0,0 +1,73 @@
+@background: #222C32;
+@success: #c7ffbc;
+// @info
+
+html, body {
+ margin: 0px;
+ background-color: @background !important;
+ width: 100%;
+ overflow-x: hidden;
+ box-sizing: border-box;
+ font-family: BlinkMacSystemFont,-apple-system,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif !important;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+.el-form-item {
+ margin-bottom: 5px;
+}
+.el-divider__text {
+ background-color: @background;
+ color: white;
+ border-radius: 5px;
+}
+
+.el-card {
+ max-width: 600px;
+ margin: 30px auto;
+}
+
+.el-dialog {
+ margin-top: 0px !important;
+ border-radius: 0px;
+ width: 100%;
+
+ .el-dialog__body {
+ word-break: break-word;
+ }
+}
+
+.el-select-dropdown {
+ max-width: 100%;
+ left: 0px;
+}
+
+.page-enter-active, .page-leave-active {
+ transition: opacity .2s, transform .3s;
+}
+.page-enter, .page-leave-active {
+ transition: opacity .3s, transform .2s;
+ opacity: 0;
+ // transform: translateY(30px);
+}
+
+pre {
+ font-family: BlinkMacSystemFont,-apple-system,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif !important;
+ margin-bottom: 0px;
+ white-space: pre-line;
+ font-family: unset;
+}
+
+.el-popover {
+ word-break: normal;
+}
+
+@media only screen and (min-width: 768px) {
+ .el-dialog {
+ margin-top: 10vh !important;
+ width: 700px;
+ border-radius: 3px;
+ }
+}
diff --git a/components/Calendar.vue b/components/Calendar.vue
new file mode 100644
index 00000000..e42e27f4
--- /dev/null
+++ b/components/Calendar.vue
@@ -0,0 +1,95 @@
+
+ #calendar
+ v-calendar(
+ title-position='left'
+ locale='it'
+ is-dark
+ :attributes='attributes'
+ :from-page.sync='page'
+ is-expanded
+ is-inline
+ @dayclick='click')
+
+
+
+
+
diff --git a/components/Event.vue b/components/Event.vue
new file mode 100644
index 00000000..c65600ef
--- /dev/null
+++ b/components/Event.vue
@@ -0,0 +1,138 @@
+
+ nuxt-link.event(:to='`event/${event.id}`' :class='{ withImg: event.image_path }')
+ //- image
+ img(v-if='showImage && event.image_path' :src='`/media/thumb/${event.image_path}`')
+
+ .event-info
+ .content-info
+
+ //- title
+ h2 {{event.title}}
+
+ //- date / place
+ .date
+ div {{event|event_when}}
+ div @{{event.place.name}}
+
+ //- p(v-if='showDescription') {{event.description}}
+
+ //- div(v-if='event.comments && event.comments.length')
+ //- v-icon(name='comments' color='dark')
+ //- span {{event.comments.length}} {{$t('common.comments')}}
+
+ ul.tags(v-if='showTags && event.tags')
+ li(v-for='tag in event.tags' :key='tag.tag') {{tag.tag}}
+
+
+
diff --git a/components/Home.vue b/components/Home.vue
new file mode 100644
index 00000000..f9c386cf
--- /dev/null
+++ b/components/Home.vue
@@ -0,0 +1,57 @@
+
+ section
+
+ a(href='#totop')
+ el-button.top.d-block.d-sm-none(icon='el-icon-top' circle type='primary' plain)
+ a.totop(name='totop')
+ no-ssr
+ Calendar
+ .row.m-0
+
+ .p-0.col-sm-6.col-lg-4.col-xl-3(v-for='event in filteredEvents')
+ a(:id='event.newDay' v-if='event.newDay')
+ .d-block.d-sm-none
+ el-divider {{event.start_datetime|day}}
+ Event(
+ :id='event.start_datetime'
+ :key='event.id'
+ :event='event'
+ )
+
+
+
+
+
diff --git a/components/List.vue b/components/List.vue
new file mode 100644
index 00000000..7a1706cf
--- /dev/null
+++ b/components/List.vue
@@ -0,0 +1,82 @@
+
+div#list
+ el-divider {{title}}
+ el-timeline
+ el-timeline-item(
+ v-for='event in events'
+ :key='event.id'
+ :timestamp='event|event_when'
+ placement='top' icon='el-icon-arrow-down' size='large'
+ )
+
+ div.float-right
+ small @{{event.place.name}}
+
+ a(:href='"/event/" + event.id' target='_blank') {{event.title}}
+ hr
+
+
+
diff --git a/components/Logo.vue b/components/Logo.vue
new file mode 100644
index 00000000..6c728541
--- /dev/null
+++ b/components/Logo.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
diff --git a/components/Nav.vue b/components/Nav.vue
new file mode 100644
index 00000000..4c50afc4
--- /dev/null
+++ b/components/Nav.vue
@@ -0,0 +1,60 @@
+
+ el-menu.d-grid.nav(mode='horizontal' router background-color="#222C32")
+
+ nuxt-link(to='/login')
+ el-menu-item(v-if='!$auth.loggedIn' index='/login' :title="$t('common.login')")
+ v-icon(color='lightgreen' name='user')
+
+ el-menu-item(index='/add' :title="$t('common.add_event')")
+ v-icon(color='lightgreen' name='plus')
+
+ el-menu-item(v-if='$auth.loggedIn' index='/settings' :title="$t('common.settings')")
+ v-icon(color='orange' name='cog')
+
+ el-menu-item(v-if='$auth.user && $auth.user.is_admin' index='/admin' :title="$t('common.admin')")
+ v-icon(color='lightblue' name='tools')
+
+ el-menu-item(index='/export' :title="$t('common.share')")
+ v-icon(name='share' color='yellow')
+
+ el-menu-item(v-if='$auth.loggedIn' @click='logout' :title="$t('common.logout')")
+ v-icon(color='red' name='sign-out-alt')
+
+ el-popover(
+ placement="bottom"
+ trigger="click")
+ Search
+ el-menu-item(slot='reference')
+ v-icon(color='lightblue' name='search')
+
+ el-menu-item.float-right(index='/about' :title="$t('common.info')")
+ img#logo(src='/favicon.ico')
+
+
+
+
+
+
diff --git a/components/README.md b/components/README.md
new file mode 100644
index 00000000..a079f106
--- /dev/null
+++ b/components/README.md
@@ -0,0 +1,7 @@
+# COMPONENTS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+The components directory contains your Vue.js Components.
+
+_Nuxt.js doesn't supercharge these components._
diff --git a/components/Search.vue b/components/Search.vue
new file mode 100644
index 00000000..5080cbd4
--- /dev/null
+++ b/components/Search.vue
@@ -0,0 +1,64 @@
+
+ div.ml-2.mt-1
+ el-switch.mb-1(v-if='$auth.loggedIn'
+ active-text='solo miei'
+ inactive-text='tutti'
+ inactive-color='lightgreen'
+ v-model='onlyMine'
+ )
+ el-switch.mt-1.mb-1.ml-2.d-block(
+ inactive-text='futuri'
+ active-text='anche passati'
+ inactive-color='lightgreen'
+ v-model='showPast'
+ )
+
+ el-select.search(v-model='filter' multiple
+ filterable collapse-tags default-first-option
+ :placeholder='$t("common.search")')
+ el-option(v-for='(keyword, id) in keywords' :key='keyword.value'
+ :label='keyword.label' :value='keyword.value')
+
+
+
+
diff --git a/config.example.js b/config.example.js
new file mode 100644
index 00000000..38ba8953
--- /dev/null
+++ b/config.example.js
@@ -0,0 +1,58 @@
+/**
+ * GANCIO CONFIGURATION
+ */
+const env = process.env.NODE_ENV || 'development'
+
+/**
+ * Database configuration
+ * `development` configuration is enabled running `yarn dev`
+ * while `production` with `yarn start`
+ * ref: http://docs.sequelizejs.com/class/lib/sequelize.js~Sequelize.html#instance-constructor-constructor
+ */
+const DB_CONF = {
+ development: {
+ storage: __dirname + '/db.sqlite',
+ dialect: 'sqlite',
+ },
+ production: {
+ username: '',
+ password: '',
+ database: 'gancio',
+ host: 'localhost',
+ dialect: 'postgres',
+ logging: false
+ },
+}
+
+const SECRET_CONF = {
+ // where events/users confirmation email are sent
+ admin: 'gancio@example.com',
+
+ db: DB_CONF[env],
+
+ // jwt salt secret (generate it randomly)
+ secret: '',
+
+ // smtp account to send email
+ smtp: {
+ host: process.env.SMTP_HOST || 'mail.example.com',
+ secure: true,
+ auth: {
+ user: process.env.SMTP_USER || 'gancio@example.com',
+ pass: process.env.SMTP_PASS || ''
+ }
+ },
+}
+
+/**
+ * Main Gancio configuration
+ */
+const SHARED_CONF = {
+ locale: 'it',
+ title: 'GANCIO',
+ description: 'A calendar for radical communities',
+ baseurl: '' || 'http://localhost:3000',
+ env
+}
+
+module.exports = { SHARED_CONF, SECRET_CONF, ...SECRET_CONF.db }
diff --git a/layouts/README.md b/layouts/README.md
new file mode 100644
index 00000000..cad1ad57
--- /dev/null
+++ b/layouts/README.md
@@ -0,0 +1,7 @@
+# LAYOUTS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your Application Layouts.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts).
diff --git a/layouts/default.vue b/layouts/default.vue
new file mode 100644
index 00000000..1a2ea589
--- /dev/null
+++ b/layouts/default.vue
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/layouts/iframe.vue b/layouts/iframe.vue
new file mode 100644
index 00000000..2cc8469e
--- /dev/null
+++ b/layouts/iframe.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/locales/email/en.json b/locales/email/en.json
new file mode 100644
index 00000000..c22030f9
--- /dev/null
+++ b/locales/email/en.json
@@ -0,0 +1,6 @@
+{
+ "registration_email": "registration_email",
+ "recover_email": "recover_email",
+ "press here": "press here",
+ "register.request": "register.request"
+}
\ No newline at end of file
diff --git a/locales/email/es.json b/locales/email/es.json
new file mode 100644
index 00000000..c22030f9
--- /dev/null
+++ b/locales/email/es.json
@@ -0,0 +1,6 @@
+{
+ "registration_email": "registration_email",
+ "recover_email": "recover_email",
+ "press here": "press here",
+ "register.request": "register.request"
+}
\ No newline at end of file
diff --git a/locales/email/it.json b/locales/email/it.json
new file mode 100644
index 00000000..c22030f9
--- /dev/null
+++ b/locales/email/it.json
@@ -0,0 +1,6 @@
+{
+ "registration_email": "registration_email",
+ "recover_email": "recover_email",
+ "press here": "press here",
+ "register.request": "register.request"
+}
\ No newline at end of file
diff --git a/locales/email/zh.json b/locales/email/zh.json
new file mode 100644
index 00000000..c22030f9
--- /dev/null
+++ b/locales/email/zh.json
@@ -0,0 +1,6 @@
+{
+ "registration_email": "registration_email",
+ "recover_email": "recover_email",
+ "press here": "press here",
+ "register.request": "register.request"
+}
\ No newline at end of file
diff --git a/locales/en.js b/locales/en.js
new file mode 100644
index 00000000..f01ae3b9
--- /dev/null
+++ b/locales/en.js
@@ -0,0 +1,3 @@
+{
+ "registration_email": "registration_email"
+}
\ No newline at end of file
diff --git a/locales/es.js b/locales/es.js
new file mode 100644
index 00000000..f01ae3b9
--- /dev/null
+++ b/locales/es.js
@@ -0,0 +1,3 @@
+{
+ "registration_email": "registration_email"
+}
\ No newline at end of file
diff --git a/locales/it.js b/locales/it.js
new file mode 100644
index 00000000..b4b881e6
--- /dev/null
+++ b/locales/it.js
@@ -0,0 +1,139 @@
+const it = {
+ common: {
+ add_event: 'Nuovo evento',
+ next: 'Continua',
+ export: 'Esporta',
+ send: 'Invia',
+ where: 'Dove',
+ address: 'Indirizzo',
+ when: 'Quando',
+ what: 'Cosa',
+ media: 'Media',
+ login: 'Entra',
+ email: 'Email',
+ password: 'Password',
+ register: 'Registrati',
+ description: 'Descrizione',
+ remove: 'Elimina',
+ hide: 'Nascondi',
+ search: 'Cerca',
+ edit: 'Modifica',
+ info: 'Info',
+ confirm: 'Conferma',
+ admin: 'Amministra',
+ users: 'Utenti',
+ events: 'Eventi',
+ places: 'Luoghi',
+ settings: 'Opzioni',
+ actions: 'Azioni',
+ deactivate: 'Disattiva',
+ remove_admin: 'Rimuovi Admin',
+ activate: 'Attiva',
+ save: 'Salva',
+ preview: 'Anteprima',
+ logout: 'Esci',
+ share: 'Esporta',
+ name: 'Nome',
+ associate: 'Associa',
+ edit_event: 'Modifica evento',
+ related: 'Memoria storica',
+ add: 'Aggiungi',
+ logout_ok: 'Uscita correttamente',
+ copy: 'Copia',
+ recover_password: 'Recupera password',
+ new_password: 'Nuova password'
+ },
+
+ login: {
+ description: `Entrando puoi pubblicare nuovi eventi.`,
+ check_email: 'Controlla la tua posta (anche lo spam)',
+ not_registered: 'Non sei registrata?',
+ forgot_password: 'Dimenticato la password?',
+ error: 'Errore: ',
+ insert_email: 'Inserisci la mail',
+ ok: 'Tutto rego'
+ },
+
+ export: {
+ intro: `Contrariamente alle piattaforme del capitalismo, che fanno di tutto per tenere
+ i dati e gli utenti al loro interno, crediamo che le informazioni, come le persone,
+ debbano essere libere. Per questo puoi rimanere aggiornata sugli eventi che vuoi, come meglio credi, senza necessariamente passare da questo sito.`,
+ email_description: `Puoi ricevere via mail gli eventi che ti interessano.`,
+ insert_your_address: 'Indirizzo email',
+ feed_description: `Per seguire gli aggiornamenti da computer o smartphone senza la necessità di aprire periodicamente il sito, il metodo consigliato è quello dei Feed RSS.
+
+ Con i feed rss utilizzi un'apposita applicazione per ricevere aggiornamenti dai siti che più ti interessano. È un buon metodo per seguire anche molti siti in modo molto rapido, senza necessità di creare un account o altre complicazioni.
+
+ Se hai Android, ti consigliamo Flym o Feeder
+ Per iPhone/iPad puoi usare Feed4U
+ Per il computer fisso/portatile consigliamo Feedbro, da installare all'interno di Firefox o di Chrome e compatibile con tutti i principali sistemi operativi.
+
+ Aggiungendo questo link al tuo lettore di feed, rimarrai aggiornata.`,
+ ical_description: `I computer e gli smartphone sono comunemente attrezzati con un'applicazione per gestire un calendario. A questi programmi solitamente è possibile far importare un calendario remoto.`,
+ list_description: `Se hai un sito web e vuoi mostrare una lista di eventi, puoi usare il seguente codice`
+ },
+
+ register: {
+ description: `I movimenti hanno bisogno di organizzarsi e autofinanziarsi.
Questo è un dono per voi, usatelo solo per eventi non commerciali e ovviamente antifascisti, antisessisti, antirazzisti.
+
Prima di poter pubblicare dobbiamo approvare l'account, considera che dietro questo sito ci sono delle persone di
+ carne e sangue, scrivici quindi due righe per farci capire che eventi vorresti pubblicare.`,
+ error: 'Errore: ',
+ admin_complete: 'Sei il primo utente e quindi sei amministratore!',
+ complete: 'Confermeremo la registrazione quanto prima.',
+ request: 'Richiesta di registrazione',
+ registration_email: `Ciao,
+ ci è arrivata una richiesta di registrazione su gancio, la confermeremo quanto prima.`
+ },
+
+ event: {
+ anon: 'Anonimo',
+ anon_description: `Puoi inserire un evento senza registrarti o fare il login,
+ ma in questo caso dovrai aspettare che qualcuno lo legga confermando che si
+ tratta di un evento adatto a questo spazio, delegando questa scelta. Inoltre non sarà possibile modificarlo.
+ Puoi invece fare il login o registrarti,
+ altrimenti vai avanti e riceverai una risposta il prima possibile. `,
+ multidate_description: 'tanti giorni',
+ date_description: `Quand'è il gancio?`,
+ dates_description: 'Che giorni?',
+ same_day: 'stesso giorno',
+ what_description: 'Nome evento',
+ description_description: 'Descrizione, dajene di copia/incolla',
+ tag_description: 'Tag...',
+ media_description: 'Puoi aggiungere un volantino',
+ time_start_description: 'Comincia alle',
+ time_end_description: 'Se vuoi puoi specificare un orario di fine.',
+ added: 'Evento aggiunto',
+ added_anon: 'Evento aggiunto, verrà confermato quanto prima.',
+ where_description: `Dov'è il gancio? Se il posto non è presente, scrivilo e premi invio. `,
+ confirmed: 'Evento confermato'
+ },
+
+ admin: {
+ mastodon_instance: 'Istanza',
+ mastodon_description: 'Puoi associare un account mastodon a questa istanza di gancio, ogni evento verrà pubblicato lì.',
+ place_description: `Nel caso in cui un luogo sia errato o cambi indirizzo, puoi modificarlo.
Considera che tutti gli eventi associati a questo luogo cambieranno indirizzo (anche quelli passati!)`,
+ event_confirm_description: 'Puoi confermare qui gli eventi inseriti da utenti anonimi'
+ },
+
+ auth: {
+ not_confirmed: 'Non abbiamo ancora confermato questa mail...',
+ fail: 'Autenticazione fallita. Sicura la password è giusta? E la mail?'
+ },
+
+ settings: {
+ change_password: 'Cambia password'
+ },
+
+ err: {
+ register_error: 'Errore nella registrazione'
+ },
+
+ firstrun: {
+ basic: `Inserisci titolo e descrizione della tua istanza di gancio.`,
+ database: `Gancio ha bisogno di un database postgresql!`,
+ smtp: `Inserisci un account SMTP relativo a questa istanza di gancio.`
+
+ }
+}
+
+export default it
diff --git a/middleware/README.md b/middleware/README.md
new file mode 100644
index 00000000..01595ded
--- /dev/null
+++ b/middleware/README.md
@@ -0,0 +1,8 @@
+# MIDDLEWARE
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your application middleware.
+Middleware let you define custom functions that can be run before rendering either a page or a group of pages.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
diff --git a/nuxt.config.js b/nuxt.config.js
new file mode 100644
index 00000000..42ae54ef
--- /dev/null
+++ b/nuxt.config.js
@@ -0,0 +1,91 @@
+const argv = require('yargs').argv
+const path = require('path')
+const config_path = path.resolve(argv.config || './config.js')
+
+const config = require(config_path).SHARED_CONF
+
+module.exports = {
+ mode: 'universal',
+ /*
+ ** Headers of the page
+ */
+ head: {
+ title: config.title,
+ meta: [
+ { charset: 'utf-8' },
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+ { hid: 'description', name: 'description', content: config.description }
+ ],
+ link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
+ },
+ dev: (process.env.NODE_ENV !== 'production'),
+ serverMiddleware: [
+ { path: '/api', handler: '@/server/api/index.js' }
+ ],
+
+ /*
+ ** Customize the progress-bar color
+ */
+ // loading: { color: '#fff' },
+
+ /*
+ ** Global CSS
+ */
+ css: [
+ 'bootstrap/dist/css/bootstrap.css',
+ 'element-ui/lib/theme-chalk/index.css'
+ ],
+ env: {
+ config
+ },
+ /*
+ ** Plugins to load before mounting the App
+ */
+ plugins: [
+ '@/plugins/element-ui', // UI library -> https://element.eleme.io/#/en-US/
+ '@/plugins/filters', // text filters, datetime, etc.
+ '@/plugins/i18n', // localization plugin
+ '@/plugins/vue-awesome', // icon
+ { src: '@/plugins/v-calendar', ssr: false } // calendar, TO-REDO
+ ],
+
+ /*
+ ** Nuxt.js modules
+ */
+ modules: [
+ // Doc: https://axios.nuxtjs.org/usage
+ '@nuxtjs/axios',
+ '@nuxtjs/auth'
+ ],
+ /*
+ ** Axios module configuration
+ */
+ axios: {
+ baseURL: config.baseurl + '/api',
+ browserBaseURL: config.baseurl + '/api',
+ prefix: '/api'
+ // credentials: true
+ // See https://github.com/nuxt-community/axios-module#options
+ },
+ auth: {
+ strategies: {
+ local: {
+ endpoints: {
+ login: { url: '/auth/login', method: 'post', propertyName: 'token' },
+ logout: false,
+ user: { url: '/auth/user', method: 'get', propertyName: false }
+ }
+ }
+ }
+ },
+
+ /*
+ ** Build configuration
+ */
+ build: {
+ transpile: [/^element-ui/, /^vue-awesome/],
+ splitChunks: {
+ layouts: true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..42c985ff
--- /dev/null
+++ b/package.json
@@ -0,0 +1,76 @@
+{
+ "name": "gancio",
+ "version": "0.9.3",
+ "description": "My well-made Nuxt.js project",
+ "author": "lesion",
+ "scripts": {
+ "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
+ "build": "nuxt build",
+ "start": "cross-env NODE_ENV=production node server/index.js",
+ "generate": "nuxt generate",
+ "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
+ "precommit": "npm run lint",
+ "migrate:dev": "sequelize db:migrate",
+ "migrate": "NODE_ENV=production sequelize db:migrate"
+ },
+ "bin": {
+ "gancio": "server/index.js"
+ },
+ "files": [
+ "server/*",
+ "nuxt.config.js",
+ "dist/*"
+ ],
+ "dependencies": {
+ "@nuxtjs/auth": "^4.6.5",
+ "@nuxtjs/axios": "^5.5.3",
+ "axios": "^0.19.0",
+ "bcrypt": "^3.0.5",
+ "body-parser": "^1.18.3",
+ "bootstrap": "4.3.1",
+ "cookie-parser": "^1.4.4",
+ "cors": "^2.8.5",
+ "cross-env": "^5.2.0",
+ "dayjs": "^1.8.14",
+ "element-ui": "^2.9.1",
+ "email-templates": "^5.1.0",
+ "express": "^4.17.1",
+ "express-jwt": "^5.3.1",
+ "ics": "^2.13.2",
+ "jsonwebtoken": "^8.5.1",
+ "less": "^3.9.0",
+ "mastodon-api": "^1.3.0",
+ "morgan": "^1.9.1",
+ "multer": "^1.4.1",
+ "nuxt": "^2.8.1",
+ "pg": "^7.11.0",
+ "sequelize": "^5.8.7",
+ "sequelize-cli": "^5.4.0",
+ "sharp": "^0.22.0",
+ "sqlite3": "^4.0.8",
+ "v-calendar": "^1.0.0-beta.14",
+ "vue-awesome": "^3.5.3",
+ "vue-i18n": "^8.10.0"
+ },
+ "devDependencies": {
+ "@nuxtjs/eslint-config": "^0.0.1",
+ "babel-eslint": "^10.0.1",
+ "eslint": "^5.15.1",
+ "eslint-config-prettier": "^4.3.0",
+ "eslint-config-standard": ">=12.0.0",
+ "eslint-loader": "^2.1.2",
+ "eslint-plugin-import": ">=2.17.3",
+ "eslint-plugin-jest": ">=22.6.4",
+ "eslint-plugin-node": ">=9.1.0",
+ "eslint-plugin-nuxt": ">=0.4.2",
+ "eslint-plugin-prettier": "^3.1.0",
+ "eslint-plugin-promise": ">=4.0.1",
+ "eslint-plugin-standard": ">=4.0.0",
+ "eslint-plugin-vue": "^5.2.2",
+ "less-loader": "^5.0.0",
+ "nodemon": "^1.19.1",
+ "prettier": "^1.17.1",
+ "pug-plain-loader": "^1.0.0",
+ "webpack-cli": "^3.3.2"
+ }
+}
diff --git a/pages/README.md b/pages/README.md
new file mode 100644
index 00000000..1d5d48b2
--- /dev/null
+++ b/pages/README.md
@@ -0,0 +1,6 @@
+# PAGES
+
+This directory contains your Application Views and Routes.
+The framework reads all the `*.vue` files inside this directory and creates the router of your application.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing).
diff --git a/pages/about.vue b/pages/about.vue
new file mode 100644
index 00000000..f0fd2b6d
--- /dev/null
+++ b/pages/about.vue
@@ -0,0 +1,50 @@
+
+ el-card
+
+ nuxt-link.float-right(to='/')
+ v-icon(name='times' color='red')
+ h3 {{$t('common.info')}}
+
+ p.
+ Gancio e' un progetto dell'underscore hacklab e uno dei
+ servizi di cisti.org.
+
+ h5 Cos'è gancio?
+ p.
+ Uno strumento di condivisione di eventi per comunità radicali.
+ Dentro gancio puoi trovare e inserire eventi.
+ Gancio, come tutto cisti.org è uno strumento
+ antisessista, antirazzista, antifascista e anticapitalista, riflettici quando
+ pubblichi un evento.
+
+ h5 Ok, ma cosa vuol dire gancio?
+ blockquote.
+ Se vieni a Torino e dici: "ehi, ci diamo un gancio alle 8?" nessuno si presenterà con i guantoni per fare a mazzate.
+ Darsi un gancio vuol dire beccarsi alle ore X in un posto Y
+ li A: a che ora è il gancio in radio per andare al presidio?
+ li B: non so ma domani non posso venire, ho gia' un gancio per caricare il bar.
+ br
+
+ h5 Contatti
+ p.
+ Hai scritto una nuova interfaccia per gancio? Vuoi aprire un nuovo nodo di gancio nella tua città?
+ C'è qualcosa che vorresti migliorare? Per contribuire i sorgenti sono liberi e disponibili
+ qui. Aiuti e suggerimenti sono sempre benvenuti, puoi scriverci
+ su underscore chicciola autistici.org
+
+
+
diff --git a/pages/add/_edit.vue b/pages/add/_edit.vue
new file mode 100644
index 00000000..11642eeb
--- /dev/null
+++ b/pages/add/_edit.vue
@@ -0,0 +1,283 @@
+
+ el-card
+ nuxt-link.float-right(to='/')
+ v-icon(name='times' color='red')
+ h5 {{edit?$t('common.edit_event'):$t('common.add_event')}}
+
+ el-form(v-loading='loading')
+ el-tabs.mb-2(v-model='activeTab')
+
+ //- NOT LOGGED EVENT
+ el-tab-pane(v-if='!$auth.loggedIn')
+ span(slot='label') {{$t('event.anon')}}
+ p(v-html="$t('event.anon_description')")
+ el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('common.next')}}
+
+ //- WHERE
+ el-tab-pane
+ span(slot='label') {{$t('common.where')}}
+ p(v-html="$t('event.where_description')")
+
+ el-select.mb-3(v-model='event.place.name'
+ @change='placeChoosed'
+ filterable allow-create
+ default-first-option
+ )
+ el-option(v-for='place in places' :label='place.name' :value='place.name' :key='place.id')
+ span {{place.name}}
+ div {{$t("common.address")}}
+ el-input.mb-3(ref='address' v-model='event.place.address'
+ :disabled='places_name.indexOf(event.place.name)>-1'
+ @keydown.native.enter='next')
+ el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('common.next')}}
+
+ //- WHEN
+ el-tab-pane
+ span(slot='label') {{$t('common.when')}}
+ span {{event.multidate ? $t('event.dates_description') : $t('event.date_description')}}
+ el-switch.float-right(v-model='event.multidate' :active-text="$t('event.multidate_description')")
+
+ v-date-picker.mb-3(
+ :mode='event.multidate ? "range" : "single"'
+ :attributes='attributes'
+ v-model='date'
+ is-inline
+ is-expanded
+ :min-date='new Date()'
+ )
+
+ el-row
+ el-col(:span='12')
+ div {{$t('event.time_start_description')}}
+ el-time-select.mb-3(ref='time_start'
+ v-model="time.start"
+ :picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
+ div {{$t('event.time_end_description')}}
+ el-time-select(v-model='time.end'
+ :picker-options="{start: '00:00', step: '00:30', end: '24:00'}")
+ el-col(:span='12')
+ List(:events='todayEvents' :title='$t("event.same_day")')
+ el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('common.next')}}
+
+ //- WHAT
+ el-tab-pane
+ span(slot='label') {{$t('common.what')}}
+ span {{$t('event.what_description')}}
+ el-input.mb-3(v-model='event.title' ref='title')
+ span {{$t('event.description_description')}}
+ el-input.mb-3(v-model='event.description' type='textarea' :rows='9')
+ span {{$t('event.tag_description')}}
+ br
+ el-select(v-model='event.tags' multiple filterable allow-create
+ default-first-option placeholder='Tag')
+ el-option(v-for='tag in tags' :key='tag.tag'
+ :label='tag' :value='tag')
+
+ el-button.float-right(@click.native='next' :disabled='!couldProceed') {{$t('common.next')}}
+
+ el-tab-pane
+ span(slot='label') {{$t('common.media')}}
+ el-upload.text-center(
+ action=''
+ :limit="1"
+ :auto-upload='false'
+ drag
+ :on-change='uploadedFile'
+ :multiple='false'
+ :file-list="fileList"
+ )
+ i.el-icon-upload
+ div.el-upload__text {{$t('event.media_description')}}
+ el-button.float-right(@click='done' :disabled='!couldProceed') {{edit?$t('common.edit'):$t('common.send')}}
+
+
+
\ No newline at end of file
diff --git a/pages/admin.vue b/pages/admin.vue
new file mode 100644
index 00000000..23e0c75a
--- /dev/null
+++ b/pages/admin.vue
@@ -0,0 +1,234 @@
+
+ el-card(:title='$t("common.admin")' width='80%' :visible='open' :before-close='close')
+ nuxt-link.float-right(to='/')
+ v-icon(name='times' color='red')
+ h5 {{$t('common.admin')}}
+
+ el-tabs(tabPosition='left' v-model='tab')
+
+ //- USERS
+ el-tab-pane.pt-1
+ template(slot='label')
+ v-icon(name='users')
+ span.ml-1 {{$t('common.users')}}
+ el-table(:data='paginatedUsers' small)
+ el-table-column(label='Email')
+ template(slot-scope='data')
+ el-popover(trigger='hover' :content='data.row.description' width='400')
+ span(slot='reference') {{data.row.email}}
+ el-table-column(:label="$t('common.actions')")
+ template(slot-scope='data')
+ el-button.mr-1(size='mini'
+ :type='data.row.is_active?"warning":"success"'
+ @click='toggle(data.row)') {{data.row.is_active?$t('common.deactivate'):$t('common.activate')}}
+ el-button(size='mini'
+ :type='data.row.is_admin?"danger":"warning"'
+ @click='toggleAdmin(data.row)') {{data.row.is_admin?$t('common.remove_admin'):$t('common.admin')}}
+ el-pagination(:page-size='perPage' :currentPage.sync='userPage' :total='users.length')
+
+ //- PLACES
+ el-tab-pane.pt-1
+ template(slot='label')
+ v-icon(name='map-marker-alt')
+ span.ml-1 {{$t('common.places')}}
+ p(v-html="$t('admin.place_description')")
+ el-form.mb-2(:inline='true' label-width='120px')
+ el-form-item(:label="$t('common.name')")
+ el-input.mr-1(:placeholder='$t("common.name")' v-model='place.name')
+ el-form-item(:label="$t('common.address')")
+ el-input.mr-1(:placeholder='$t("common.address")' v-model='place.address')
+ el-button(variant='primary' @click='savePlace') {{$t('common.save')}}
+ el-table(:data='paginatedPlaces' small @current-change="val => place=val")
+ el-table-column(:label="$t('common.name')")
+ template(slot-scope='data') {{data.row.name}}
+ el-table-column(:label="$t('common.address')")
+ template(slot-scope='data') {{data.row.address}}
+ el-pagination(:page-size='perPage' :currentPage.sync='placePage' :total='places.length')
+
+ //- EVENTS
+ el-tab-pane.pt-1
+ template(slot='label')
+ v-icon(name='calendar')
+ span.ml-1 {{$t('common.events')}}
+ p {{$t('admin.event_confirm_description')}}
+ el-table(:data='paginatedEvents' small primary-key='id' v-loading='loading')
+ el-table-column(:label='$t("common.name")')
+ template(slot-scope='data') {{data.row.title}}
+ el-table-column(:label='$t("common.where")')
+ template(slot-scope='data') {{data.row.place.name}}
+ el-table-column(:label='$t("common.confirm")')
+ template(slot-scope='data')
+ el-button(type='primary' @click='confirm(data.row.id)' size='mini') {{$t('common.confirm')}}
+ el-button(type='success' @click='preview(data.row.id)' size='mini') {{$t('common.preview')}}
+
+ el-pagination(:page-size='perPage' :currentPage.sync='eventPage' :total='events.length')
+
+ //- TAGS
+ //- el-tab-pane.pt-1
+ //- template(slot='label')
+ //- v-icon(name='tags')
+ //- span {{$t('common.tags')}}
+ //- p {{$t('admin.tag_description')}}
+ //- el-tag(v-if='tag.tag' :color='tag.color' size='mini') {{tag.tag}}
+ //- el-form(:inline='true' label-width='120px')
+ //- el-form-item(:label="$t('common.color')")
+ //- el-color-picker(v-model='tag.color' @change='updateColor')
+ //- el-table(:data='paginatedTags' striped small hover
+ //- highlight-current-row @current-change="tagSelected")
+ //- el-table-column(:label="$t('common.tag')")
+ //- template(slot-scope='data')
+ //- el-tag(:color='data.row.color' size='mini') {{data.row.tag}}
+ //- el-pagination(:page-size='perPage' :currentPage.sync='tagPage' :total='tags.length')
+
+ //- SETTINGS
+ el-tab-pane.pt-1
+ template(slot='label')
+ v-icon(name='cog')
+ span {{$t('common.settings')}}
+
+ //- el-form(inline @submit.prevent.stop='save_settings' label-width='140px')
+ //- p {{$t('settings.name_description')}}
+ //- el-form-item(:label="$t('settings.name')")
+ //- el-input(v-model="settings.title")
+ //- el-form-item(:label="$t('settings.description')")
+ //- el-input(v-model="settings.description")
+ //- el-button(slot='append' @click='associate' :disabled='!mastodon_instance.length') {{$t('common.associate')}}
+
+ el-form(inline @submit.prevent.stop='associatemastodon_instance')
+ span {{$t('admin.mastodon_description')}}
+ el-input(v-model="settings.mastodon_instance")
+ span(slot='prepend') {{$t('admin.mastodon_instance')}}
+ el-button(slot='append' @click='associate' :disabled='!mastodon_instance.length') {{$t('common.associate')}}
+
+
+
\ No newline at end of file
diff --git a/pages/embed/list.vue b/pages/embed/list.vue
new file mode 100644
index 00000000..1e711364
--- /dev/null
+++ b/pages/embed/list.vue
@@ -0,0 +1,29 @@
+
+ List(:events="events" :title='title')
+
+
diff --git a/pages/event/_id.vue b/pages/event/_id.vue
new file mode 100644
index 00000000..5e168453
--- /dev/null
+++ b/pages/event/_id.vue
@@ -0,0 +1,201 @@
+
+ el-card#eventDetail
+
+ //- close button
+ nuxt-link.float-right(to='/')
+ el-button(circle icon='el-icon-close' type='danger' size='small' plain)
+
+ //- title, where, when
+ h5.text-center {{event.title}}
+ div.nextprev
+ nuxt-link(v-if='prev' :to='`/event/${prev.id}`')
+ el-button(icon='el-icon-arrow-left' round type='success')
+ nuxt-link.float-right(v-if='next' :to='`/event/${next.id}`')
+ el-button(icon='el-icon-arrow-right' round type='success')
+
+ //- image
+ img(:src='imgPath' v-if='event.image_path')
+
+ .info
+ div {{event|event_when}}
+ div {{event.place.name}} - {{event.place.address}}
+
+ //- description and tags
+ div(v-if='event.description || event.tags')
+ pre(v-html='event.description')
+ el-tag.mr-1(v-for='tag in event.tags'
+ size='mini' :key='tag.tag') {{tag.tag}}
+
+ //- show hide, confirm, delete, edit buttons when allowed
+ div(v-if='mine')
+ hr
+ el-button(v-if='event.is_visible' size='mini' plain type='warning' @click.prevents='toggle' icon='el-icon-view') {{$t('common.hide')}}
+ el-button(v-else plain type='success' size='mini' @click.prevents='toggle' icon='el-icon-view') {{$t('common.confirm')}}
+ el-button(plain type='danger' size='mini' @click.prevent='remove' icon='el-icon-remove') {{$t('common.remove')}}
+ el-button(plain type='primary' size='mini' @click='$router.replace(`/add/${event.id}`)' icon='el-icon-edit') {{$t('common.edit')}}
+
+ //- comments
+ .card-body(v-if='event.activitypub_id')
+ strong {{$t('common.related')}} -
+ a(:href='`https://mastodon.cisti.org/web/statuses/${event.activitypub_id}`') {{$t('common.add')}}
+ .card-header(v-for='comment in event.comments' :key='comment.id')
+ img.avatar(:src='comment.data.last_status.account.avatar')
+ strong {{comment.author}}
+ a.float-right(:href='comment.data.last_status.url')
+ small {{comment.data.last_status.created_at|datetime}}
+ div.mt-1(v-html='comment_filter(comment.text)')
+ img(v-for='img in comment.data.last_status.media_attachments' :src='img.preview_url')
+
+
+
+
+
diff --git a/pages/export.vue b/pages/export.vue
new file mode 100644
index 00000000..bacef4b8
--- /dev/null
+++ b/pages/export.vue
@@ -0,0 +1,142 @@
+
+ el-card
+
+ nuxt-link.float-right(to='/')
+ v-icon(name='times' color='red')
+ h5 {{$t('common.export')}}
+
+
+ p {{$t('export.intro')}}
+ Search
+ //- li(v-if='filters.tags.length') {{$t('common.tags')}}:
+ //- el-tag.ml-1(size='mini' v-for='tag in filters.tags' :key='tag.tag') {{tag}}
+ //- li(v-if='filters.places.length') {{$t('common.places')}}:
+ //- el-tag.ml-1(size='mini' v-for='place in filters.places' :key='place.id') {{place}}
+ el-tabs.mt-2(v-model='type')
+
+ el-tab-pane.pt-1(label='email' name='email')
+ p(v-html='$t(`export.email_description`)')
+ el-form(@submit.native.prevent)
+ //- el-switch(v-model='notification.notify_on_add' :active-text="$t('notify_on_insert')")
+ //- br
+ //- el-switch.mt-2(v-model='notification.send_notification' :active-text="$t('send_notification')")
+ el-input.mt-2(v-model='notification.email' :placeholder="$t('export.insert_your_address')" ref='email')
+ el-button.mt-2.float-right(native-type= 'submit' type='success' @click='add_notification') {{$t('common.send')}}
+
+ el-tab-pane.pt-1(label='feed rss' name='feed')
+ span(v-html='$t(`export.feed_description`)')
+ el-input(v-model='link')
+ el-button(slot='append' plain type="primary" icon='el-icon-document' ) {{$t("common.copy")}}
+
+ el-tab-pane.pt-1(label='ics/ical' name='ics')
+ p(v-html='$t(`export.ical_description`)')
+ el-input(v-model='link')
+ el-button(slot='append' plain type="primary" icon='el-icon-document') {{$t("common.copy")}}
+
+ el-tab-pane.pt-1(label='list' name='list')
+ p(v-html='$t(`export.list_description`)')
+
+ el-row
+ el-col.mr-2(:span='11')
+ el-input(v-model='list.title') Title
+ el-col.float-right(:span='12')
+ List(
+ :title='list.title'
+ :events='filteredEvents'
+ )
+ el-input.mb-1(type='textarea' v-model='listScript' readonly )
+ el-button.float-right(plain type="primary" icon='el-icon-document') {{$t('common.copy')}}
+
+
+ //- el-tab-pane.pt-1(label='calendar' name='calendar')
+ //- p(v-html='$t(`export.calendar_description`)')
+ //- //- no-ssr
+ //- Calendar.mb-1
+ //- el-input.mb-1(type='textarea' v-model='script')
+ //- el-button.float-right(plain type="primary" icon='el-icon-document') Copy
+
+
+
+
+
+
diff --git a/pages/index.vue b/pages/index.vue
new file mode 100644
index 00000000..a74231c1
--- /dev/null
+++ b/pages/index.vue
@@ -0,0 +1,29 @@
+
+ #home
+ Nav
+ Home
+
+
+
+
diff --git a/pages/login.vue b/pages/login.vue
new file mode 100644
index 00000000..82742265
--- /dev/null
+++ b/pages/login.vue
@@ -0,0 +1,83 @@
+
+
+ el-card
+
+ nuxt-link.float-right(to='/')
+ v-icon(name='times' color='red')
+ h5 {{$t('common.login')}}
+
+ el-form(v-loading='loading' method='POST' action='/api/auth/login')
+ p(v-html="$t('login.description')")
+
+ el-input.mb-2(v-model='email' type='email' name='email'
+ :placeholder='$t("common.email")' autocomplete='email' ref='email')
+ v-icon(name='user' slot='prepend')
+
+ el-input.mb-1(v-model='password' @keyup.enter.native="submit" name='password'
+ type='password' :placeholder='$t("common.password")')
+ v-icon(name='lock' slot='prepend')
+
+ el-button.mr-1(plain type="success" native-type='submit'
+ :disabled='disabled' @click='submit') {{$t('common.login')}}
+
+ nuxt-link(to='/register')
+ el-button.mt-1(plain type="primary") {{$t('login.not_registered')}}
+
+ a.float-right(href='#' @click='forgot') {{$t('login.forgot_password')}}
+
+
+
diff --git a/pages/recover/_code.vue b/pages/recover/_code.vue
new file mode 100644
index 00000000..01c337c9
--- /dev/null
+++ b/pages/recover/_code.vue
@@ -0,0 +1,54 @@
+
+ el-card
+ nuxt-link.float-right(to='/')
+ el-button(circle icon='el-icon-close' type='danger' size='small' plain)
+
+ h5
{{$t('common.recover_password')}}
+ div(v-if='valid')
+ el-form
+ el-form-item {{$t('common.new_password')}}
+ el-input(type='password', v-model='new_password')
+ el-button(plain type="success" icon='el-icon-send', @click='change_password') {{$t('common.send')}}
+
+ div(v-else) {{$t('recover.not_valid_code')}}
+
+
+
+
+
+
diff --git a/pages/register.vue b/pages/register.vue
new file mode 100644
index 00000000..691355b2
--- /dev/null
+++ b/pages/register.vue
@@ -0,0 +1,63 @@
+
+ el-card
+
+ nuxt-link.float-right(to='/')
+ v-icon(name='times' color='red')
+ h5 {{$t('common.register')}}
+
+ el-form(@submit.native.prevent='register' method='POST' action='/api/user')
+ p(v-html="$t('register.description')")
+ el-input.mb-2(ref='email' v-model='user.email' type='email' required
+ :placeholder='$t("common.email")' autocomplete='email' name='email')
+ span(slot='prepend') @
+
+ el-input.mb-2(v-model='user.password' type="password" placeholder="Password" name='password' required)
+ v-icon(name='lock' slot='prepend')
+
+ el-input.mb-2(v-model='user.description' type="textarea" rows='3' :placeholder="$t('common.description')")
+ v-icon(name='envelope-open-text')
+
+ el-button(plain type="success" native-type='submit'
+ :disabled='disabled') {{$t('common.send')}}
+
+
+
diff --git a/pages/settings.vue b/pages/settings.vue
new file mode 100644
index 00000000..80269052
--- /dev/null
+++ b/pages/settings.vue
@@ -0,0 +1,46 @@
+
+ el-card
+ nuxt-link.float-right(to='/')
+ v-icon(name='times' color='red')
+ h5 {{$t('common.settings')}}
+
+ //- el-form
+ //- el-form-item {{$t('settings.change_password')}}
+ el-divider {{$t('settings.change_password')}}
+ el-input(v-model='password' type='password')
+ el-button(slot='append' @click='change' type='success') {{$t('common.send')}}
+
+
+
diff --git a/plugins/README.md b/plugins/README.md
new file mode 100644
index 00000000..ca1f9d8a
--- /dev/null
+++ b/plugins/README.md
@@ -0,0 +1,7 @@
+# PLUGINS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains Javascript plugins that you want to run before mounting the root Vue.js application.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).
diff --git a/plugins/element-ui.js b/plugins/element-ui.js
new file mode 100644
index 00000000..7293e6fe
--- /dev/null
+++ b/plugins/element-ui.js
@@ -0,0 +1,50 @@
+import Vue from 'vue'
+import { Button, Select, Tag, Option, Table, FormItem, Card, Row, Col, Upload, Checkbox,
+ Form, Tabs, TabPane, Switch, Input, Loading, TimeSelect, Badge, ButtonGroup, Divider, Step, Steps,
+ TableColumn, ColorPicker, Pagination, Popover, Tooltip, Dialog, Calendar, Carousel, CarouselItem,
+ Container, Footer , Timeline, TimelineItem, Menu, MenuItem } from 'element-ui'
+import localeEn from 'element-ui/lib/locale/lang/en'
+import localeIt from 'element-ui/lib/locale/lang/it'
+import locale from 'element-ui/lib/locale'
+locale.use(localeIt)
+
+export default () => {
+ Vue.use(Button)
+ Vue.use(Divider)
+ Vue.use(Step)
+ Vue.use(Steps)
+ Vue.use(Checkbox)
+ Vue.use(Upload)
+ Vue.use(ButtonGroup)
+ Vue.use(Calendar)
+ Vue.use(Row)
+ Vue.use(Col)
+ Vue.use(Carousel)
+ Vue.use(CarouselItem)
+ Vue.use(Badge)
+ Vue.use(Dialog)
+ Vue.use(Menu)
+ Vue.use(MenuItem)
+ Vue.use(Container)
+ Vue.use(Timeline)
+ Vue.use(TimelineItem)
+ Vue.use(Footer)
+ Vue.use(Tooltip)
+ Vue.use(Popover)
+ Vue.use(Card)
+ Vue.use(Select)
+ Vue.use(Tag)
+ Vue.use(Input)
+ Vue.use(Tabs)
+ Vue.use(TabPane)
+ Vue.use(Option)
+ Vue.use(Switch)
+ Vue.use(ColorPicker)
+ Vue.use(Table)
+ Vue.use(TableColumn)
+ Vue.use(Pagination)
+ Vue.use(FormItem)
+ Vue.use(Form)
+ Vue.use(TimeSelect)
+ Vue.use(Loading.directive)
+}
diff --git a/plugins/filters.js b/plugins/filters.js
new file mode 100644
index 00000000..5dc4d672
--- /dev/null
+++ b/plugins/filters.js
@@ -0,0 +1,31 @@
+import Vue from 'vue'
+import moment from 'dayjs'
+import 'dayjs/locale/it'
+moment.locale('it')
+
+function short_hour(datetime) {
+ if (datetime.minute() === 0) {
+ return 'h' + datetime.format('HH')
+ } else {
+ return 'h' + datetime.format('HH:mm')
+ }
+}
+
+export default (a) => {
+ Vue.filter('linkify', value => value.replace(/(https?:\/\/[^\s]+)/g, '$1'))
+ Vue.filter('datetime', value => moment(value).format('ddd, D MMMM HH:mm'))
+ Vue.filter('short_datetime', value => moment(value).format('D/MM HH:mm'))
+ Vue.filter('hour', value => moment(value).format('HH:mm'))
+ Vue.filter('day', value => moment(value).format('dddd, D MMMM'))
+ Vue.filter('month', value => moment(value).format('MMM'))
+ Vue.filter('event_when', event => {
+ const start = moment(event.start_datetime)
+ const end = moment(event.end_datetime)
+ if (event.multidate) {
+ return `${start.format('ddd, D MMMM')} (${short_hour(start)}) - ${end.format('ddd, D MMMM')} (${short_hour(end)})`
+ } else if (event.end_datetime && event.end_datetime !== event.start_datetime)
+ return `${start.format('ddd, D MMMM')} (${short_hour(start)}-${short_hour(end)}`
+ else
+ return `${start.format('dddd, D MMMM')} (${short_hour(start)})`
+ })
+}
diff --git a/plugins/i18n.js b/plugins/i18n.js
new file mode 100644
index 00000000..03dc32b4
--- /dev/null
+++ b/plugins/i18n.js
@@ -0,0 +1,25 @@
+import Vue from 'vue'
+import VueI18n from 'vue-i18n'
+import it from '@/locales/it.js'
+
+Vue.use(VueI18n)
+
+export default ({ app, store }) => {
+ // Set i18n instance on app
+ // This way we can use it in middleware and pages asyncData/fetch
+ app.i18n = new VueI18n({
+ locale: store.state.locale,
+ fallbackLocale: 'it',
+ messages: {
+ it
+ }
+ })
+
+ // app.i18n.path = (link) => {
+ // if (app.i18n.locale === app.i18n.fallbackLocale) {
+ // return `/${link}`
+ // }
+
+ // return `/${app.i18n.locale}/${link}`
+ // }
+}
diff --git a/plugins/initialize.js b/plugins/initialize.js
new file mode 100644
index 00000000..821bfe83
--- /dev/null
+++ b/plugins/initialize.js
@@ -0,0 +1,9 @@
+
+// TOFIX: not needed in any case (eg. embed)
+export default async ({ store, $axios }) => {
+ // const now = new Date()
+ // const events = await $axios.$get(`/event/${now.getMonth()}/${now.getFullYear()}`)
+ // store.commit('setEvents', events)
+ // const { tags, places } = await $axios.$get('/event/meta')
+ // store.commit('update', { tags, places })
+}
diff --git a/plugins/v-calendar.js b/plugins/v-calendar.js
new file mode 100644
index 00000000..14ecafaf
--- /dev/null
+++ b/plugins/v-calendar.js
@@ -0,0 +1,8 @@
+import Vue from 'vue'
+import VCalendar from 'v-calendar'
+
+export default () => {
+ Vue.use(VCalendar, {
+ firstDayOfWeek: 2
+ })
+}
diff --git a/plugins/vue-awesome.js b/plugins/vue-awesome.js
new file mode 100644
index 00000000..2dac2a63
--- /dev/null
+++ b/plugins/vue-awesome.js
@@ -0,0 +1,33 @@
+import Vue from 'vue'
+import 'vue-awesome/icons/lock'
+import 'vue-awesome/icons/user'
+import 'vue-awesome/icons/plus'
+import 'vue-awesome/icons/cog'
+import 'vue-awesome/icons/tools'
+import 'vue-awesome/icons/file-export'
+import 'vue-awesome/icons/sign-out-alt'
+import 'vue-awesome/icons/clock'
+import 'vue-awesome/icons/map-marker-alt'
+import 'vue-awesome/icons/file-alt'
+import 'vue-awesome/icons/image'
+import 'vue-awesome/icons/tag'
+import 'vue-awesome/icons/users'
+import 'vue-awesome/icons/calendar'
+import 'vue-awesome/icons/edit'
+import 'vue-awesome/icons/envelope-open-text'
+import 'vue-awesome/icons/user-secret'
+import 'vue-awesome/icons/question-circle'
+import 'vue-awesome/icons/share'
+import 'vue-awesome/icons/comment'
+import 'vue-awesome/icons/comments'
+import 'vue-awesome/icons/tags'
+import 'vue-awesome/icons/chevron-right'
+import 'vue-awesome/icons/chevron-left'
+import 'vue-awesome/icons/search'
+import 'vue-awesome/icons/times'
+
+import Icon from 'vue-awesome/components/Icon'
+
+export default () => {
+ Vue.component('v-icon', Icon)
+}
diff --git a/plugins/vuex-persist.js b/plugins/vuex-persist.js
new file mode 100644
index 00000000..a0cf9450
--- /dev/null
+++ b/plugins/vuex-persist.js
@@ -0,0 +1,9 @@
+// // ~/plugins/vuex-persist.js
+// import VuexPersistence from 'vuex-persist'
+
+// export default ({ store }) => {
+// const per = new VuexPersistence({
+// reducer: state => ({ logged: state.logged, user: state.user, token: state.token })
+// }).plugin(store)
+// store.dispatch('login')
+// }
diff --git a/server/api/auth.js b/server/api/auth.js
new file mode 100644
index 00000000..7e7a8cdc
--- /dev/null
+++ b/server/api/auth.js
@@ -0,0 +1,52 @@
+const { Op } = require('sequelize')
+const { user: User } = require('./models')
+const Settings = require('./controller/settings')
+
+const Auth = {
+ async fillUser(req, res, next) {
+ if (!req.user) return next()
+ req.user = await User.findOne({
+ where: { id: { [Op.eq]: req.user.id }, is_active: true }
+ }).catch(e => {
+ res.sendStatus(404)
+ return next(false)
+ })
+ next()
+ },
+ 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 ' + err })
+ }
+ next()
+ },
+ isAdmin(req, res, next) {
+ if (!req.user) {
+ return res
+ .status(403)
+ .send({ message: 'Failed to authenticate token ' })
+ }
+ if (req.user.is_admin && req.user.is_active) return next()
+ return res.status(403).send({ message: 'Admin needed' })
+ },
+ async adminOrFirstRun(req, res, next) {
+ if (req.user && req.user.is_admin && req.user.is_active) return next()
+ const settings = await Settings.settings()
+ if (!settings.firstRun) {
+ return next()
+ }
+ }
+
+}
+
+module.exports = Auth
diff --git a/server/api/controller/bot.js b/server/api/controller/bot.js
new file mode 100644
index 00000000..a1b8ba81
--- /dev/null
+++ b/server/api/controller/bot.js
@@ -0,0 +1,95 @@
+const fs = require('fs')
+const path = require('path')
+const moment = require('moment')
+const { event: Event, comment: Comment, tag: Tag } = require('../model')
+const config = require('../../config').SHARED_CONF
+const Mastodon = require('mastodon-api')
+const settingsController = require('./settings')
+moment.locale('it')
+
+const botController = {
+ bot: null,
+ async initialize() {
+ console.error('dentro bot inizialiteds')
+ const settings = await settingsController.settings()
+ if (!settings.mastodon_auth || !settings.mastodon_auth.access_token) return
+ const mastodon_auth = settings.mastodon_auth
+ botController.bot = new Mastodon({
+ access_token: mastodon_auth.access_token,
+ api_url: `https://${mastodon_auth.instance}/api/v1`
+ })
+ const listener = botController.bot.stream('/streaming/direct')
+ listener.on('message', botController.message)
+ listener.on('error', botController.error)
+ // 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)
+ // 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: 'direct', media_ids: media ? [media.data.id] : [] })
+ },
+
+ // TOFIX: enable message deletion
+ async message(msg) {
+ 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: { }})
+ return
+ }
+ const comment = await Comment.create({
+ activitypub_id: msg.data.last_status.id,
+ // text: msg.data.last_status.content,
+ data: msg.data
+ // author: msg.data.accounts[0].username
+ })
+ event.addComment(comment)
+ // 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
diff --git a/server/api/controller/event.js b/server/api/controller/event.js
new file mode 100644
index 00000000..fd83e4e2
--- /dev/null
+++ b/server/api/controller/event.js
@@ -0,0 +1,194 @@
+const crypto = require('crypto')
+const moment = require('moment')
+const { Op } = require('sequelize')
+const lodash = require('lodash')
+const { event: Event, comment: Comment, tag: Tag, place: Place, notification: Notification } = require('../models')
+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: { [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({
+ order: [[Sequelize.literal('weigth'), 'DESC']],
+ attributes: {
+ include: [[Sequelize.fn('count', Sequelize.col('events.placeId')) , 'weigth']], // <---- Here you will get the total count of user
+ exclude: ['weigth', 'createdAt', 'updatedAt']
+ },
+ include: [{ model: Event, attributes: [] }],
+ group: ['place.id']
+ })
+
+ const tags = await Tag.findAll({
+ order: [['weigth', 'DESC']],
+ includeIgnoreAttributes: false,
+ attributes: {
+ exclude: ['createdAt', 'updatedAt']
+ }
+ })
+
+ 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:
+ [
+ Tag,
+ Comment,
+ { model: Place, attributes: ['name', 'address'] }
+ ],
+ order: [ [Comment, 'id', 'DESC'], [Tag, 'weigth', 'DESC'] ]
+ })
+ 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) {
+ // 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'],
+ [Tag, 'weigth', 'DESC']
+ ],
+ include: [
+ // { model: User, required: false },
+ // { type: Comment, required: false, attributes: ['']
+ { model: Tag, required: false, attributes: ['tag', 'weigth', 'color'] },
+ { model: Place, required: false, attributes: ['id', 'name', 'address'] }
+ ]
+ })
+ res.json(events)
+ }
+
+}
+
+module.exports = eventController
diff --git a/server/api/controller/export.js b/server/api/controller/export.js
new file mode 100644
index 00000000..f0b5ba26
--- /dev/null
+++ b/server/api/controller/export.js
@@ -0,0 +1,70 @@
+const { event: Event, place: Place } = require('../models')
+const { Op } = require('sequelize')
+const moment = require('moment')
+const ics = require('ics')
+
+const exportController = {
+
+ async export(req, res) {
+ console.log('type ', req.params.type)
+ console.error(req)
+ 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.id = places.split(',')
+ }
+ const events = await Event.findAll({
+ order: ['start_datetime'],
+ where: {
+ is_visible: true,
+ start_datetime: { [Op.gte]: yesterday },
+ placeId: places.split(',')
+ },
+ attributes: {
+ exclude: ['createdAt', 'updatedAt']
+ },
+ include: [{ model: Place, attributes: ['name', 'id', 'address', 'weigth'] }]
+ })
+ switch (type) {
+ case 'feed':
+ return exportController.feed(res, events.slice(0, 20))
+ case 'ics':
+ return exportController.ics(res, events)
+ case 'json':
+ return res.json(events)
+ }
+ },
+
+ feed(res, events) {
+ res.type('application/rss+xml; charset=UTF-8')
+ res.render('feed/rss.pug', { events, config: process.env.config, moment })
+ },
+
+ 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)
+ res.send(value)
+ }
+}
+
+module.exports = exportController
diff --git a/server/api/controller/settings.js b/server/api/controller/settings.js
new file mode 100644
index 00000000..4830c85f
--- /dev/null
+++ b/server/api/controller/settings.js
@@ -0,0 +1,61 @@
+const Mastodon = require('mastodon-api')
+const { setting: Setting } = require('../models')
+
+const baseurl = process.env.baseurl
+
+const settingsController = {
+
+ async setAdminSetting(key, value) {
+ await Setting.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 getAuthURL(req, res) {
+ const instance = req.body.instance
+ const callback = `${baseurl}/api/settings/oauth`
+ const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`,
+ 'gancio', 'read write', callback)
+ const url = await Mastodon.getAuthorizationUrl(client_id, client_secret,
+ `https://${instance}`, 'read write', callback)
+
+ await settingsController.setAdminSetting('mastodon_auth', { client_id, client_secret, instance })
+ res.json(url)
+ },
+
+ async code(req, res) {
+ const code = req.query.code
+ let client_id, client_secret, instance
+ const callback = `${baseurl}/api/settings/oauth`
+
+ const settings = await settingsController.settings()
+
+ ({ client_id, client_secret, instance } = settings.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 }
+ await settingsController.setAdminSetting('mastodon_auth', mastodon_auth)
+
+ res.redirect('/admin')
+ } catch (e) {
+ res.json(e)
+ }
+ },
+
+ async settings() {
+ const settings = await Setting.findAll()
+ return settings
+ }
+
+}
+
+module.exports = settingsController
diff --git a/server/api/controller/user.js b/server/api/controller/user.js
new file mode 100644
index 00000000..f8668450
--- /dev/null
+++ b/server/api/controller/user.js
@@ -0,0 +1,250 @@
+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 mail = require('../mail')
+const { user: User, event: Event, tag: Tag, place: Place } = require('../models')
+const eventController = require('./event')
+const config = require('../../config')
+
+const userController = {
+ async login(req, res) {
+ // find the user
+ const user = await User.findOne({ where: { email: { [Op.eq]: req.body && 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_CONF.secret
+ )
+
+ res.json({ token: accessToken })
+ }
+ }
+ },
+
+ async logout(req, res) {
+
+ },
+
+ 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)
+ try {
+ await fs.unlink(old_path)
+ await fs.unlink(old_thumb_path)
+ } catch (e) {
+ console.error(e)
+ }
+ }
+ await event.destroy()
+ res.sendStatus(200)
+ } else {
+ res.sendStatus(403)
+ }
+ },
+
+ // ADD EVENT
+ async addEvent(req, res) {
+ const body = req.body
+
+ const eventDetails = {
+ title: body.title,
+ description: body.description.replace(/(<([^>]+)>)/ig, ''),
+ multidate: body.multidate,
+ start_datetime: body.start_datetime,
+ end_datetime: body.end_datetime,
+
+ // publish this event if authenticated
+ is_visible: !!req.user
+ }
+
+ if (req.file) {
+ eventDetails.image_path = req.file.filename
+ }
+
+ let event = await Event.create(eventDetails)
+
+ // create place if needs to
+ 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, '$1') // 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 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: config.SHARED_CONF })
+
+ 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
+ const password = req.body.password
+ if (!recover_code || !password) return res.sendStatus(400)
+ const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
+ if (!user) return res.sendStatus(400)
+ user.password = password
+ try {
+ await user.save()
+ res.sendStatus(200)
+ } catch (e) {
+ res.sendStatus(400)
+ }
+ },
+
+ current(req, res) {
+ if (req.user) { res.json(req.user) } else { res.sendStatus(404) }
+ },
+
+ 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)
+ console.error(req.body.id)
+ if (user) {
+ if (!user.is_active && req.body.is_active) {
+ await mail.send(user.email, 'confirm', { user, config: config.SHARED_CONF })
+ }
+ await user.update(req.body)
+ res.json(user)
+ } else {
+ res.sendStatus(400)
+ }
+ },
+
+ async register(req, res) {
+ const n_users = await User.count()
+ try {
+ // the first registered user will be an active admin
+ if (n_users === 0) {
+ 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.SECRET_CONF.admin], 'register', { user, config: config.SHARED_CONF })
+ } catch (e) {
+ return res.status(400).json(e)
+ }
+ const payload = {
+ id: user.id,
+ email: user.email,
+ scope: [user.is_admin ? 'admin' : 'user']
+ }
+ const token = jwt.sign(payload, config.SECRET_CONF.secret)
+ res.json({ token, user })
+ } catch (e) {
+ res.status(404).json(e)
+ }
+ }
+}
+
+module.exports = userController
diff --git a/server/api/index.js b/server/api/index.js
new file mode 100644
index 00000000..29a74c13
--- /dev/null
+++ b/server/api/index.js
@@ -0,0 +1,107 @@
+const express = require('express')
+const multer = require('multer')
+const cookieParser = require('cookie-parser')
+const bodyParser = require('body-parser')
+const expressJwt = require('express-jwt')
+const config = require('../config')
+
+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 storage = require('./storage')({
+ destination: 'uploads/'
+})
+
+const upload = multer({ storage })
+const api = express.Router()
+api.use(cookieParser())
+api.use(bodyParser.urlencoded({ extended: false }))
+api.use(bodyParser.json())
+
+const jwt = expressJwt({
+ secret: config.SECRET_CONF.secret,
+ credentialsRequired: false
+})
+
+function errorHandler(fn) {
+ return async (req, res) => {
+ try {
+ await fn(req, res)
+ } catch (e) {
+ console.error(String(e))
+ return res.status(500).json(e)
+ }
+ }
+}
+
+// AUTH
+api.post('/auth/login', userController.login)
+api.post('/auth/logout', userController.logout)
+api.get('/auth/user', jwt, fillUser, userController.current)
+
+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(jwt, isAuth, isAdmin, userController.update)
+
+// get all users
+api.get('/users', jwt, isAuth, isAdmin, userController.getAll)
+
+// update a tag (modify color)
+api.put('/tag', jwt, isAuth, isAdmin, eventController.updateTag)
+
+// update a place (modify address..)
+api.put('/place', jwt, isAuth, isAdmin, eventController.updatePlace)
+
+api
+ .route('/user/event')
+ // add event
+ .post(jwt, fillUser, upload.single('image'), userController.addEvent)
+ // update event
+ .put(jwt, isAuth, upload.single('image'), userController.updateEvent)
+
+// remove event
+api.delete('/user/event/:id', jwt, isAuth, userController.delEvent)
+
+// get tags/places
+api.get('/event/meta', eventController.getMeta)
+
+// get unconfirmed events
+api.get('/event/unconfirmed', jwt, isAuth, isAdmin, eventController.getUnconfirmed)
+
+// add event notification
+api.post('/event/notification', eventController.addNotification)
+api.delete('/event/notification/:code', eventController.delNotification)
+
+api.get('/settings', jwt, fillUser, isAdmin, settingsController.getAdminSettings)
+api.post('/settings', jwt, fillUser, isAdmin, settingsController.setAdminSetting)
+
+// get event
+api.get('/event/:event_id', eventController.get)
+
+// confirm event
+api.get('/event/confirm/:event_id', jwt, isAuth, isAdmin, eventController.confirm)
+api.get('/event/unconfirm/:event_id', jwt, isAuth, isAdmin, eventController.unconfirm)
+
+// export events (rss/ics)
+api.get('/export/:type', exportController.export)
+
+// get events in this range
+api.get('/event/:month/:year', errorHandler(eventController.getAll))
+
+// mastodon oauth auth
+api.post('/settings/getauthurl', jwt, isAuth, isAdmin, settingsController.getAuthURL)
+api.get('/settings/oauth', jwt, isAuth, isAdmin, settingsController.code)
+
+module.exports = api
diff --git a/server/api/mail.js b/server/api/mail.js
new file mode 100644
index 00000000..bba20259
--- /dev/null
+++ b/server/api/mail.js
@@ -0,0 +1,49 @@
+const Email = require('email-templates')
+const path = require('path')
+const moment = require('moment')
+const config = require('../config')
+
+moment.locale(config.SHARED_CONF.locale)
+const mail = {
+ send(addresses, template, locals) {
+ const email = new Email({
+ views: { root: path.join(__dirname, '..', 'emails') },
+ htmlToText: false,
+ juice: true,
+ juiceResources: {
+ preserveImportant: true,
+ webResources: {
+ relativeTo: path.join(__dirname, '..', 'emails')
+ }
+ },
+ message: {
+ from: `${config.SHARED_CONF.title} <${config.SECRET_CONF.smtp.auth.user}>`
+ },
+ send: true,
+ i18n: {
+ directory: path.join(__dirname, '..', '..', 'locales', 'email'),
+ defaultLocale: config.SHARED_CONF.locale
+ },
+ transport: config.SECRET_CONF.smtp
+ })
+ const msg = {
+ template,
+ message: {
+ to: addresses,
+ bcc: config.SECRET_CONF.admin
+ },
+ locals: {
+ ...locals,
+ locale: config.SHARED_CONF.locale,
+ config: config.SHARED_CONF,
+ datetime: datetime => moment(datetime).format('ddd, D MMMM HH:mm')
+ }
+ }
+ return email.send(msg)
+ .catch(e => {
+ console.error(e)
+ })
+ }
+}
+
+module.exports = mail
diff --git a/server/api/models/comment.js b/server/api/models/comment.js
new file mode 100644
index 00000000..acba9a7c
--- /dev/null
+++ b/server/api/models/comment.js
@@ -0,0 +1,13 @@
+'use strict'
+module.exports = (sequelize, DataTypes) => {
+ const comment = sequelize.define('comment', {
+ activitypub_id: DataTypes.BIGINT,
+ data: DataTypes.JSON
+ }, {})
+ comment.associate = function (models) {
+ comment.belongsTo(models.event)
+ // Event.hasMany(Comment)
+ // associations can be defined here
+ }
+ return comment
+};
diff --git a/server/api/models/event.js b/server/api/models/event.js
new file mode 100644
index 00000000..8af98bdc
--- /dev/null
+++ b/server/api/models/event.js
@@ -0,0 +1,31 @@
+'use strict'
+module.exports = (sequelize, DataTypes) => {
+ const event = sequelize.define('event', {
+ title: DataTypes.STRING,
+ slug: DataTypes.STRING,
+ description: DataTypes.TEXT,
+ multidate: DataTypes.BOOLEAN,
+ start_datetime: {
+ type: DataTypes.DATE,
+ index: true
+ },
+ end_datetime: DataTypes.DATE,
+ image_path: DataTypes.STRING,
+ is_visible: DataTypes.BOOLEAN,
+ activitypub_id: {
+ type: DataTypes.BIGINT,
+ index: true
+ }
+ }, {})
+ event.associate = function (models) {
+ event.belongsTo(models.place)
+ event.belongsTo(models.user)
+ event.belongsToMany(models.tag, { through: 'event_tags' })
+ event.belongsToMany(models.notification, { through: 'event_notification' })
+ event.hasMany(models.comment)
+ // Tag.belongsToMany(Event, { through: 'tagEvent' })
+ // Event.hasMany(models.Tag)
+ // associations can be defined here
+ }
+ return event
+};
diff --git a/server/api/models/eventnotification.js b/server/api/models/eventnotification.js
new file mode 100644
index 00000000..bc8f9ecb
--- /dev/null
+++ b/server/api/models/eventnotification.js
@@ -0,0 +1,16 @@
+'use strict'
+module.exports = (sequelize, DataTypes) => {
+ const eventNotification = sequelize.define('eventNotification', {
+ status: {
+ type: DataTypes.ENUM,
+ values: ['new', 'sent', 'error'],
+ defaultValue: 'new',
+ index: true
+ }
+ }, {})
+
+ eventNotification.associate = function (models) {
+ // associations can be defined here
+ }
+ return eventNotification
+}
diff --git a/server/api/models/index.js b/server/api/models/index.js
new file mode 100644
index 00000000..d1a23202
--- /dev/null
+++ b/server/api/models/index.js
@@ -0,0 +1,31 @@
+const argv = require('yargs').argv
+const fs = require('fs')
+const path = require('path')
+const Sequelize = require('sequelize')
+const config_path = path.resolve(argv.config || './config.js')
+const basename = path.basename(__filename)
+const config = require(config_path).SECRET_CONF.db
+const db = {}
+
+const sequelize = new Sequelize(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
diff --git a/server/api/models/notification.js b/server/api/models/notification.js
new file mode 100644
index 00000000..e49bc9f4
--- /dev/null
+++ b/server/api/models/notification.js
@@ -0,0 +1,17 @@
+'use strict'
+module.exports = (sequelize, DataTypes) => {
+ const notification = sequelize.define('notification', {
+ filters: DataTypes.JSON,
+ email: DataTypes.STRING,
+ remove_code: DataTypes.STRING,
+ type: {
+ type: DataTypes.ENUM,
+ values: ['mail', 'admin_email', 'mastodon']
+ }
+ }, {})
+ notification.associate = function (models) {
+ notification.belongsToMany(models.event, { through: 'event_notification' })
+ // associations can be defined here
+ }
+ return notification
+}
diff --git a/server/api/models/place.js b/server/api/models/place.js
new file mode 100644
index 00000000..a45e9704
--- /dev/null
+++ b/server/api/models/place.js
@@ -0,0 +1,15 @@
+'use strict'
+module.exports = (sequelize, DataTypes) => {
+ const place = sequelize.define('place', {
+ name: DataTypes.STRING,
+ address: DataTypes.STRING,
+ weigth: DataTypes.INTEGER
+ }, {})
+
+ place.associate = function (models) {
+ // associations can be defined here
+ place.hasMany(models.event)
+ }
+
+ return place
+}
diff --git a/server/api/models/setting.js b/server/api/models/setting.js
new file mode 100644
index 00000000..eef0d5a0
--- /dev/null
+++ b/server/api/models/setting.js
@@ -0,0 +1,14 @@
+'use strict'
+module.exports = (sequelize, DataTypes) => {
+ const setting = sequelize.define('setting', {
+ key: {
+ type: DataTypes.STRING,
+ primaryKey: true,
+ allowNull: false,
+ index: true
+ },
+ value: DataTypes.JSON
+ }, {})
+
+ return setting
+}
diff --git a/server/api/models/tag.js b/server/api/models/tag.js
new file mode 100644
index 00000000..ba8f9807
--- /dev/null
+++ b/server/api/models/tag.js
@@ -0,0 +1,19 @@
+'use strict'
+module.exports = (sequelize, DataTypes) => {
+ const tag = sequelize.define('tag', {
+ tag: {
+ type: DataTypes.STRING,
+ index: true,
+ primaryKey: true
+ },
+ weigth: DataTypes.INTEGER,
+ color: DataTypes.STRING
+ }, {})
+
+ tag.associate = function (models) {
+ tag.belongsToMany(models.event, { through: 'event_tags' })
+ // associations can be defined here
+ }
+
+ return tag
+};
diff --git a/server/api/models/user.js b/server/api/models/user.js
new file mode 100644
index 00000000..dff5f86b
--- /dev/null
+++ b/server/api/models/user.js
@@ -0,0 +1,43 @@
+'use strict'
+const bcrypt = require('bcrypt')
+
+module.exports = (sequelize, DataTypes) => {
+ const user = sequelize.define('user', {
+ email: {
+ type: DataTypes.STRING,
+ unique: true,
+ index: true,
+ allowNull: false
+ },
+ description: DataTypes.TEXT,
+ password: DataTypes.STRING,
+ recover_code: DataTypes.STRING,
+ is_admin: DataTypes.BOOLEAN,
+ is_active: DataTypes.BOOLEAN
+ }, {
+ defaultScope: {
+ attributes: { exclude: ['password', 'recover_code'] }
+ }
+ })
+
+ user.associate = function (models) {
+ // associations can be defined here
+ user.hasMany(models.event)
+ }
+
+ 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
+ }
+ })
+
+ return user
+};
diff --git a/server/api/storage.js b/server/api/storage.js
new file mode 100644
index 00000000..0fba8074
--- /dev/null
+++ b/server/api/storage.js
@@ -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: 90 })
+ const thumbnailer = sharp().resize(400).jpeg({ quality: 90 })
+
+ 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)
+}
diff --git a/server/api/views/feed/rss.pug b/server/api/views/feed/rss.pug
new file mode 100644
index 00000000..26429080
--- /dev/null
+++ b/server/api/views/feed/rss.pug
@@ -0,0 +1,23 @@
+doctype xml
+rss(version='2.0')
+ channel
+ title #{config.title}
+ link #{config.baseurl}
+ 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 #{config.baseurl}/event/#{event.id}
+ description
+ | #{event.title}
+ | #{event.place.name} - #{event.place.address}
+ | #{moment(event.start_datetime).format("dddd, D MMMM HH:mm")}
+ |
+ | !{event.description}
+ | ]]>
+ pubDate= new Date(event.createdAt).toUTCString()
+ guid(isPermaLink='false') #{config.baseurl}/event/#{event.id}
\ No newline at end of file
diff --git a/server/emails/confirm/html.pug b/server/emails/confirm/html.pug
new file mode 100644
index 00000000..e0f26aa1
--- /dev/null
+++ b/server/emails/confirm/html.pug
@@ -0,0 +1,4 @@
+p= t('confirm_email')
+
+hr
+small #{config.baseurl}
diff --git a/server/emails/event/html.pug b/server/emails/event/html.pug
new file mode 100644
index 00000000..2fefccc6
--- /dev/null
+++ b/server/emails/event/html.pug
@@ -0,0 +1,18 @@
+h3 #{event.title}
+p Dove: #{event.place.name} - #{event.place.address}
+p Quando: #{datetime(event.start_datetime)}
+br
+if event.image_path
+
+p #{event.description}
+
+each tag in event.tags
+ span ##{tag.tag}
+br
+#{config.baseurl}/event/#{event.id}
+hr
+if to_confirm
+ p Puoi confermare questo evento qui
+else
+ p Puoi eliminare queste notifiche qui
+#{config.title} - #{config.description}
diff --git a/server/emails/event/subject.pug b/server/emails/event/subject.pug
new file mode 100644
index 00000000..fd159324
--- /dev/null
+++ b/server/emails/event/subject.pug
@@ -0,0 +1 @@
+= `[${config.title}] ${event.title} @${event.place.name} ${datetime(event.start_datetime)}`
diff --git a/server/emails/mail.css b/server/emails/mail.css
new file mode 100644
index 00000000..6dd4fb48
--- /dev/null
+++ b/server/emails/mail.css
@@ -0,0 +1,8 @@
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+table, th, td {
+ border: 1px solid #555;
+}
diff --git a/server/emails/recover/html.pug b/server/emails/recover/html.pug
new file mode 100644
index 00000000..0ef71822
--- /dev/null
+++ b/server/emails/recover/html.pug
@@ -0,0 +1,3 @@
+p= t('mail.recover')
+
+#{t('press here')}
\ No newline at end of file
diff --git a/server/emails/recover/subject.pug b/server/emails/recover/subject.pug
new file mode 100644
index 00000000..6d0067a6
--- /dev/null
+++ b/server/emails/recover/subject.pug
@@ -0,0 +1 @@
+= `[Gancio] Richiesta password recovery`
diff --git a/server/emails/register/html.pug b/server/emails/register/html.pug
new file mode 100644
index 00000000..8cdb9def
--- /dev/null
+++ b/server/emails/register/html.pug
@@ -0,0 +1,6 @@
+p= t('registration_email')
+
+hr
+small #{config.title} / #{config.description}
+br
+small #{config.baseurl}
\ No newline at end of file
diff --git a/server/emails/register/subject.pug b/server/emails/register/subject.pug
new file mode 100644
index 00000000..88d6807a
--- /dev/null
+++ b/server/emails/register/subject.pug
@@ -0,0 +1 @@
+| [Gancio] #{t('register.request')}
diff --git a/server/emails/register/text.pug b/server/emails/register/text.pug
new file mode 100644
index 00000000..1a72986e
--- /dev/null
+++ b/server/emails/register/text.pug
@@ -0,0 +1 @@
+| dioicane ciao
\ No newline at end of file
diff --git a/server/firstrun.js b/server/firstrun.js
new file mode 100644
index 00000000..502912dc
--- /dev/null
+++ b/server/firstrun.js
@@ -0,0 +1,41 @@
+// check config.js existance
+const fs = require('fs')
+const path = require('path')
+const argv = require('yargs').argv
+
+const config_path = path.resolve(argv.config || './config.js')
+
+if (!fs.existsSync(config_path)) {
+ console.error(`Configuration file not found at '${config_path}. Please copy 'config.example.js' and modify it.`)
+ process.exit(1)
+}
+
+const { SECRET_CONF, SHARED_CONF } = require(config_path)
+if (!SECRET_CONF.secret) {
+ console.error(`Please specify a random 'secret' in '${config_path}'!`)
+ process.exit(1)
+}
+
+const Sequelize = require('sequelize')
+let db
+try {
+ db = new Sequelize(SECRET_CONF.db)
+} catch (e) {
+ console.error(`DB Error: check '${SHARED_CONF.env}' configuration.\n (sequelize error -> ${e})`)
+ process.exit(1)
+}
+
+// return db existence
+module.exports = db.authenticate()
+ .then(() => {
+ require('./api/models')
+ if (SHARED_CONF.env === 'development') {
+ console.error('DB Force sync')
+ return db.sync({ force: true })
+ }
+ })
+ .catch(e => {
+ console.error(e)
+ console.error(`DB Error: check '${SHARED_CONF.env}' configuration\n (sequelize error -> ${e})`)
+ process.exit(1)
+ })
diff --git a/server/index.js b/server/index.js
new file mode 100644
index 00000000..d205da28
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,39 @@
+#!/bin/env node
+const path = require('path')
+const express = require('express')
+const consola = require('consola')
+const morgan = require('morgan')
+const { Nuxt, Builder } = require('nuxt')
+const firstRun = require('./firstrun')
+// Import and Set Nuxt.js options
+const config = require('../nuxt.config.js')
+
+const app = express()
+async function start() {
+ // Init Nuxt.js
+ const nuxt = new Nuxt(config)
+
+ const { host, port } = nuxt.options.server
+
+ // Build only in dev mode
+ if (config.dev) {
+ const builder = new Builder(nuxt)
+ await builder.build()
+ } else {
+ await nuxt.ready()
+ }
+
+ // Give nuxt middleware to express
+ app.use(morgan('dev'))
+ app.use('/media/', express.static(path.join(__dirname, '..', 'uploads')))
+ app.use(nuxt.render)
+
+ // Listen the server
+ app.listen(port, host)
+ consola.ready({
+ message: `Server listening on http://${host}:${port}`,
+ badge: true
+ })
+}
+
+firstRun.then(start)
diff --git a/server/migrations/20190605141112-create-user.js b/server/migrations/20190605141112-create-user.js
new file mode 100644
index 00000000..9f10631b
--- /dev/null
+++ b/server/migrations/20190605141112-create-user.js
@@ -0,0 +1,45 @@
+'use strict';
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('users', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ email: {
+ type: Sequelize.STRING,
+ unique: true,
+ index: true,
+ allowNull: false
+ },
+ description: {
+ type: Sequelize.TEXT
+ },
+ password: {
+ type: Sequelize.STRING
+ },
+ recover_code: {
+ type: Sequelize.STRING
+ },
+ is_admin: {
+ type: Sequelize.BOOLEAN
+ },
+ is_active: {
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('users');
+ }
+};
\ No newline at end of file
diff --git a/server/migrations/20190605141800-create-place.js b/server/migrations/20190605141800-create-place.js
new file mode 100644
index 00000000..ee73e440
--- /dev/null
+++ b/server/migrations/20190605141800-create-place.js
@@ -0,0 +1,33 @@
+'use strict';
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('places', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ name: {
+ type: Sequelize.STRING
+ },
+ address: {
+ type: Sequelize.STRING
+ },
+ weigth: {
+ type: Sequelize.INTEGER
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('places');
+ }
+};
\ No newline at end of file
diff --git a/server/migrations/20190605141850-create-event.js b/server/migrations/20190605141850-create-event.js
new file mode 100644
index 00000000..2db6ee51
--- /dev/null
+++ b/server/migrations/20190605141850-create-event.js
@@ -0,0 +1,66 @@
+'use strict'
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('events', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ title: {
+ type: Sequelize.STRING
+ },
+ slug: {
+ type: Sequelize.STRING,
+ index: true
+ },
+ description: {
+ type: Sequelize.TEXT
+ },
+ multidate: {
+ type: Sequelize.BOOLEAN
+ },
+ start_datetime: {
+ type: Sequelize.DATE
+ },
+ end_datetime: {
+ type: Sequelize.DATE
+ },
+ image_path: {
+ type: Sequelize.STRING
+ },
+ is_visible: {
+ type: Sequelize.BOOLEAN
+ },
+ activitypub_id: {
+ type: Sequelize.BIGINT
+ },
+ userId: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'users',
+ key: 'id'
+ }
+ },
+ placeId: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'places',
+ key: 'id'
+ }
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ })
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('events')
+ }
+}
diff --git a/server/migrations/20190605142152-create-notification.js b/server/migrations/20190605142152-create-notification.js
new file mode 100644
index 00000000..7ddd5129
--- /dev/null
+++ b/server/migrations/20190605142152-create-notification.js
@@ -0,0 +1,37 @@
+'use strict'
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('notifications', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ filters: {
+ type: Sequelize.JSON
+ },
+ email: {
+ type: Sequelize.STRING
+ },
+ remove_code: {
+ type: Sequelize.STRING
+ },
+ type: {
+ type: Sequelize.ENUM,
+ values: ['mail', 'admin_email', 'mastodon']
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ })
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('notifications')
+ }
+}
diff --git a/server/migrations/20190605142317-create-tag.js b/server/migrations/20190605142317-create-tag.js
new file mode 100644
index 00000000..82271aaf
--- /dev/null
+++ b/server/migrations/20190605142317-create-tag.js
@@ -0,0 +1,29 @@
+'use strict';
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('tags', {
+ tag: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ primaryKey: true
+ },
+ weigth: {
+ type: Sequelize.INTEGER
+ },
+ color: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('tags');
+ }
+};
\ No newline at end of file
diff --git a/server/migrations/20190605142409-create-event-notification.js b/server/migrations/20190605142409-create-event-notification.js
new file mode 100644
index 00000000..5cebe1e4
--- /dev/null
+++ b/server/migrations/20190605142409-create-event-notification.js
@@ -0,0 +1,37 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('event_notification', {
+ eventId: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'events',
+ key: 'id'
+ }
+ },
+ notificationId: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'notifications',
+ key: 'id'
+ }
+ },
+ status: {
+ type: Sequelize.ENUM,
+ values: ['new', 'sent', 'error'],
+ defaultValue: 'new',
+ index: true
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ })
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('event_notification')
+ }
+}
diff --git a/server/migrations/20190605142434-create-comment.js b/server/migrations/20190605142434-create-comment.js
new file mode 100644
index 00000000..120e0a62
--- /dev/null
+++ b/server/migrations/20190605142434-create-comment.js
@@ -0,0 +1,38 @@
+'use strict';
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('comments', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ eventId: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'events',
+ key: 'id'
+ }
+ },
+ activitypub_id: {
+ type: Sequelize.BIGINT,
+ index: true,
+ },
+ data: {
+ type: Sequelize.JSON
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('comments');
+ }
+};
\ No newline at end of file
diff --git a/server/migrations/20190605142619-create-setting.js b/server/migrations/20190605142619-create-setting.js
new file mode 100644
index 00000000..fc95a51d
--- /dev/null
+++ b/server/migrations/20190605142619-create-setting.js
@@ -0,0 +1,27 @@
+'use strict'
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('settings', {
+ key: {
+ type: Sequelize.STRING,
+ primaryKey: true,
+ allowNull: false,
+ index: true
+ },
+ value: {
+ type: Sequelize.JSON
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ })
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('settings')
+ }
+}
diff --git a/server/migrations/20190605160024-create-event-tag.js b/server/migrations/20190605160024-create-event-tag.js
new file mode 100644
index 00000000..c3c904ef
--- /dev/null
+++ b/server/migrations/20190605160024-create-event-tag.js
@@ -0,0 +1,32 @@
+'use strict'
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('event_tags', {
+ eventId: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'events',
+ key: 'id'
+ }
+ },
+ tagTag: {
+ type: Sequelize.STRING,
+ references: {
+ model: 'tags',
+ key: 'tag'
+ }
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ })
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('event_tags')
+ }
+}
diff --git a/server/notifier.js b/server/notifier.js
new file mode 100644
index 00000000..24cb1075
--- /dev/null
+++ b/server/notifier.js
@@ -0,0 +1,66 @@
+// const mail = require('./mail')
+const bot = require('./api/controller/bot')
+const settingsController = require('./api/controller/settings')
+const config = require('./config.js')
+
+const { Event, Notification, EventNotification,
+ User, Place, Tag } = require('./api/models')
+let settings
+
+async function sendNotification(notification, event, eventNotification) {
+ const promises = []
+ switch (notification.type) {
+ // case 'mail':
+ // return mail.send(notification.email, 'event', { event, config, notification })
+ // case 'admin_email':
+ // const admins = await User.findAll({ where: { is_admin: true } })
+ // const admin_emails = admins.map(admin => admin.email)
+ // return mail.send(admin_emails, 'event', { event, to_confirm: true, notification })
+ case 'mastodon':
+ // instance publish
+ if (settings.mastodon_auth.instance && settings.mastodon_auth.access_token) {
+ const b = bot.post(settings.mastodon_auth, event).then(b => {
+ event.activitypub_id = b.data.id
+ // event.activitypub_ids.push(b.data.id)
+ return event.save()
+ })
+ promises.push(b)
+ }
+ }
+ return Promise.all(promises)
+}
+
+async function notify() {
+ console.error('dentro il loop di notify')
+ settings = await settingsController.settings()
+ // get all event notification in queue
+ const eventNotifications = await EventNotification.findAll({ where: { status: 'new' } })
+ const promises = eventNotifications.map(async e => {
+ const event = await Event.findByPk(e.eventId, { include: [User, Place, Tag] })
+ if (!event.place) return
+ const notification = await Notification.findByPk(e.notificationId)
+ try {
+ await sendNotification(notification, event, e)
+ e.status = 'sent'
+ return e.save()
+ } catch (err) {
+ console.error(err)
+ e.status = 'error'
+ return e.save()
+ }
+ })
+
+ return Promise.all(promises)
+}
+
+let interval
+function startLoop(seconds) {
+ console.error('starting notifier loop')
+ interval = setInterval(notify, seconds * 1000)
+}
+
+function stopLoop() {
+ stopInterval(interval)
+}
+
+module.exports = { startLoop, stopLoop }
diff --git a/static/README.md b/static/README.md
new file mode 100644
index 00000000..cf004353
--- /dev/null
+++ b/static/README.md
@@ -0,0 +1,11 @@
+# STATIC
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your static files.
+Each file inside this directory is mapped to `/`.
+Thus you'd want to delete this README.md before deploying to production.
+
+Example: `/static/robots.txt` is mapped as `/robots.txt`.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static).
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 00000000..36b55d4a
Binary files /dev/null and b/static/favicon.ico differ
diff --git a/static/gancio.png b/static/gancio.png
new file mode 100644
index 00000000..ef88fd64
Binary files /dev/null and b/static/gancio.png differ
diff --git a/store/README.md b/store/README.md
new file mode 100644
index 00000000..1972d277
--- /dev/null
+++ b/store/README.md
@@ -0,0 +1,10 @@
+# STORE
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your Vuex Store files.
+Vuex Store option is implemented in the Nuxt.js framework.
+
+Creating a file in this directory automatically activates the option in the framework.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
diff --git a/store/index.js b/store/index.js
new file mode 100644
index 00000000..48e082d2
--- /dev/null
+++ b/store/index.js
@@ -0,0 +1,143 @@
+import moment from 'dayjs'
+import intersection from 'lodash/intersection'
+import map from 'lodash/map'
+
+export const state = () => ({
+ config: {},
+ events: [],
+ tags: [],
+ places: [],
+ settings: {},
+ filters: {
+ tags: [],
+ places: [],
+ show_past_events: false,
+ show_recurrent_events: false,
+ show_pinned_event: false
+ }
+})
+
+export const getters = {
+
+ // filter current + future events only
+ // plus, filter matches search tag/place
+ filteredEvents: (state) => {
+ let events = state.events
+
+ // TOFIX: use lodash
+ if (state.filters.tags.length || state.filters.places.length) {
+ events = events.filter((e) => {
+ if (state.filters.tags.length) {
+ const m = intersection(e.tags.map(t => t.tag), state.filters.tags)
+ if (m.length > 0) return true
+ }
+ if (state.filters.places.length) {
+ if (state.filters.places.find(p => p === e.place.id)) {
+ return true
+ }
+ }
+ return 0
+ })
+ }
+
+ if (!state.show_past_events) {
+ events = events.filter(e => !e.past)
+ }
+
+ let lastDay = null
+ events = map(events, e => {
+ const currentDay = moment(e.start_datetime).date()
+ e.newDay = (!lastDay || lastDay !== currentDay) && currentDay
+ lastDay = currentDay
+ return e
+ })
+
+ return events
+ }
+}
+
+export const mutations = {
+ setEvents(state, events) {
+ // set a `past` flag
+ state.events = events.map((e) => {
+ const end_datetime = e.end_datetime || moment(e.start_datetime).add('3', 'hour')
+ const past = (moment().diff(end_datetime, 'minutes') > 0)
+ e.past = !!past
+ return e
+ })
+ },
+ addEvent(state, event) {
+ state.events.push(event)
+ },
+ updateEvent(state, event) {
+ state.events = state.events.map((e) => {
+ if (e.id !== event.id) return e
+ return event
+ })
+ },
+ delEvent(state, eventId) {
+ state.events = state.events.filter(ev => {
+ return ev.id !== eventId
+ })
+ },
+ update(state, { tags, places }) {
+ state.tags = tags
+ state.places = places
+ },
+ setSearchTags(state, tags) {
+ state.filters.tags = tags
+ },
+ setSearchPlaces(state, places) {
+ state.filters.places = places
+ },
+ showPastEvents(state, show) {
+ state.show_past_events = show
+ },
+ setSettings(state, settings) {
+ state.settings = settings
+ },
+ setConfig(state, config) {
+ state.config = config
+ }
+}
+
+export const actions = {
+ async updateEvents({ commit }, page) {
+ const events = await this.$axios.$get(`/event/${page.month - 1}/${page.year}`)
+ commit('setEvents', events)
+ },
+ async updateMeta({ commit }) {
+ const { tags, places } = await this.$axios.$get('/event/meta')
+ commit('update', { tags, places })
+ },
+ async addEvent({ commit }, formData) {
+ const event = await this.$axios.$post('/user/event', formData)
+ if (event.user) {
+ commit('addEvent', event)
+ }
+ },
+ async updateEvent({ commit }, formData) {
+ const event = await this.$axios.$put('/user/event', formData)
+ if (event.user) {
+ commit('updateEvent', event)
+ }
+ },
+ delEvent({ commit }, eventId) {
+ commit('delEvent', eventId)
+ },
+ setSearchTags({ commit }, tags) {
+ commit('setSearchTags', tags)
+ },
+ setSearchPlaces({ commit }, places) {
+ commit('setSearchPlaces', places)
+ },
+ showPastEvents({ commit }, show) {
+ commit('showPastEvents', show)
+ },
+ setSettings({ commit }, settings) {
+ commit('setSettings', settings)
+ },
+ setConfig({ commit }, config) {
+ commit('setConfig', config)
+ }
+}