From 88b43f9bb117376a9b475ba5375efe4199d177a4 Mon Sep 17 00:00:00 2001 From: lesion Date: Wed, 3 Apr 2019 00:25:12 +0200 Subject: [PATCH] start with nuxt --- .editorconfig | 13 ++ .gitignore | 84 +++++++++ .prettierrc | 4 + assets/README.md | 7 + components/Calendar.vue | 94 ++++++++++ components/Event.vue | 64 +++++++ components/Home.vue | 100 +++++++++++ components/Logo.vue | 79 +++++++++ components/Nav.vue | 50 ++++++ components/README.md | 7 + components/Search.vue | 39 ++++ db.sqlite | Bin 0 -> 69632 bytes layouts/README.md | 7 + layouts/default.vue | 85 +++++++++ middleware/README.md | 8 + nuxt.config.js | 80 +++++++++ package.json | 58 ++++++ pages/Login.vue | 65 +++++++ pages/README.md | 6 + pages/Register.vue | 60 +++++++ pages/event/_id.vue | 121 +++++++++++++ pages/index.vue | 2 + pages/new_event.vue | 215 +++++++++++++++++++++++ plugins/README.md | 7 + plugins/api.js | 82 +++++++++ plugins/bootstrap-vue.js | 6 + plugins/element-ui.js | 26 +++ plugins/filters.js | 8 + plugins/i18n.js | 25 +++ plugins/magic-grid.js | 6 + plugins/v-calendar.js | 9 + plugins/vue-awesome.js | 24 +++ server/api/auth.js | 48 +++++ server/api/config.js | 27 +++ server/api/config/config.json | 14 ++ server/api/controller/bot.js | 82 +++++++++ server/api/controller/event.js | 169 ++++++++++++++++++ server/api/controller/export.js | 64 +++++++ server/api/controller/settings.js | 27 +++ server/api/controller/user.js | 283 ++++++++++++++++++++++++++++++ server/api/db.js | 5 + server/api/index.js | 86 +++++++++ server/api/mail.js | 44 +++++ server/api/model.js | 14 ++ server/api/models/event.js | 72 ++++++++ server/api/models/index.js | 37 ++++ server/api/models/settings.js | 9 + server/api/models/user.js | 34 ++++ server/api/storage.js | 62 +++++++ server/index.js | 36 ++++ static/README.md | 11 ++ static/favicon.ico | Bin 0 -> 1150 bytes store/README.md | 10 ++ store/index.js | 167 ++++++++++++++++++ 54 files changed, 2742 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 assets/README.md create mode 100644 components/Calendar.vue create mode 100644 components/Event.vue create mode 100644 components/Home.vue create mode 100644 components/Logo.vue create mode 100644 components/Nav.vue create mode 100644 components/README.md create mode 100644 components/Search.vue create mode 100644 db.sqlite create mode 100644 layouts/README.md create mode 100644 layouts/default.vue create mode 100644 middleware/README.md create mode 100644 nuxt.config.js create mode 100644 package.json create mode 100644 pages/Login.vue create mode 100644 pages/README.md create mode 100644 pages/Register.vue create mode 100644 pages/event/_id.vue create mode 100644 pages/index.vue create mode 100644 pages/new_event.vue create mode 100644 plugins/README.md create mode 100644 plugins/api.js create mode 100644 plugins/bootstrap-vue.js create mode 100644 plugins/element-ui.js create mode 100644 plugins/filters.js create mode 100644 plugins/i18n.js create mode 100644 plugins/magic-grid.js create mode 100644 plugins/v-calendar.js create mode 100644 plugins/vue-awesome.js create mode 100644 server/api/auth.js create mode 100644 server/api/config.js create mode 100644 server/api/config/config.json create mode 100644 server/api/controller/bot.js create mode 100644 server/api/controller/event.js create mode 100644 server/api/controller/export.js create mode 100644 server/api/controller/settings.js create mode 100644 server/api/controller/user.js create mode 100644 server/api/db.js create mode 100644 server/api/index.js create mode 100644 server/api/mail.js create mode 100644 server/api/model.js create mode 100644 server/api/models/event.js create mode 100644 server/api/models/index.js create mode 100644 server/api/models/settings.js create mode 100644 server/api/models/user.js create mode 100644 server/api/storage.js create mode 100644 server/index.js create mode 100644 static/README.md create mode 100644 static/favicon.ico create mode 100644 store/README.md create mode 100644 store/index.js 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/.gitignore b/.gitignore new file mode 100644 index 00000000..f935a370 --- /dev/null +++ b/.gitignore @@ -0,0 +1,84 @@ +# Created by .ignore support plugin (hsz.mobi) +### 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 + +# 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/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/components/Calendar.vue b/components/Calendar.vue new file mode 100644 index 00000000..3dfcfb31 --- /dev/null +++ b/components/Calendar.vue @@ -0,0 +1,94 @@ + + + + diff --git a/components/Event.vue b/components/Event.vue new file mode 100644 index 00000000..db25a65b --- /dev/null +++ b/components/Event.vue @@ -0,0 +1,64 @@ + + + diff --git a/components/Home.vue b/components/Home.vue new file mode 100644 index 00000000..2cdda1dd --- /dev/null +++ b/components/Home.vue @@ -0,0 +1,100 @@ + + + + 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..892755e5 --- /dev/null +++ b/components/Nav.vue @@ -0,0 +1,50 @@ + + + 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..fbc3ed2d --- /dev/null +++ b/components/Search.vue @@ -0,0 +1,39 @@ + + + diff --git a/db.sqlite b/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..2ad224f1f0719c35d78bc084c3415593d7d922cc GIT binary patch literal 69632 zcmeI(U2oG?7{KwEv`JrRQzlhbHB~awMC+9f0z(r*(-wl+vXvClbXzZ`#3ZIz%S(x! z0vEez8QS$e#lFXGxY|eIYC;-!dyZ3Aab7m6Y|>=?BaR&(JI;B2&pF2lPIn$`xxQmO zYImBpZ(L6-B{VJZxnU#{i8oR%NC_W*$djpXL)Kc~^Knn#BtDwHy^#4cv6OtBSiH3O zEc5%q=EB?g&AIRAr)DqBe3<$*{dVfN)WhWKsb9vOgn9%J_!k6@E?!9MtE<}avTr|Z zIG*GCZfoBQ*Gu`5l`C6DId^-@GOA&9)mX3o;5@Gy-{ea9FLR~!Yd3C~#{E*^Uaquf zd~NMrHmZkqqw7?SuXeVJvQ_Iiw(rz)e$}`mCoC84Sw?ZYY!r95wq$qrpgwlDdF8#7 zo?q1xZmaG*^PV(hcB!Zua1;j;@bRx@cmo7G-AK~UDA)AEaL-+koPY~O9S zyy2R~XrRLfsE|G1_PbuyC|lo^jXT!e-0oJ{SZg^?*CvZlUhSwh7V1@_P%K+tSfx`O zqh#H+N>(v%?VKDX4Op+b^{Q!X7o`o^U-8P_MPwCFFC5? zG~0(xrPi*?{L?=}$v*%2fmEsiEE^Ma1oQag`Cj+C6~ta^H=8noy>LAjjX116OR#p$ zcMn}@aQ9(FhI!al11doAXSj*MWd1h*93j8Jw2hX*SxidK3HbbYvcVFq_tw zm$l=KaMa4Ay6??Ixm1;+l65p7W4Bm%uxkxmYHIDq2#yINH`nG;`mN=0Gn8sxFbure z7eCIV^_3Ov=iMNlz{~3`)6tlEm1pSyzUw!JdvM+HY904LUB^y4a_-<2nhL>KI*;ak@4!3FVJCy_5e;lP({cdcJbiK-<>$x%s476Z^ zB*+_fJ*U%u$?ETYYTu|U)cDRPCp>7_HD}xzgPmtPrFm3H1^GX22Kn!uqPLum=09@w za4`3GWMgo)YuPGi{p0zh`>Ryc>mA3F#`f=)`U$+vjdV)?Y-L=h@3p^@?X6SylPP^; zL)#0&KKW4QIZwJytL6-BPDLOFcCM=^FUu><GB{_D-(Ybr^7^1tgjsqg z#}QYF(~nm|Hflf}*q-;a-H};$z_;Vn0R#|0009ILKmY**5I`VK0=?h=ze;3Y#VIOcK>z^+5I_I{1Q0*~0R#|0 zAeI83Xp4#WmlGFNa2I7+ciT0)<U^eE*MSS_ujP1Q0*~0R#|0009IL zKmdV=0Du1<;a~#+1Q0*~0R#|0009ILKmdVQ3UL2FmT4s@1Q0*~0R#|0009ILKmY** zA_Cn1k8rSo00IagfB*srAb + #app + Nav + Home + transition(name="fade" mode="out-in") + //- router-view(name='modal') + nuxt + + + + + 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..952fe72d --- /dev/null +++ b/nuxt.config.js @@ -0,0 +1,80 @@ +const pkg = require('./package') + +module.exports = { + mode: 'universal', + + /* + ** Headers of the page + */ + head: { + title: pkg.name, + meta: [ + { charset: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { hid: 'description', name: 'description', content: pkg.description } + ], + link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }] + }, + + serverMiddleware: [{ path: '/api', handler: '@/server/api/index.js' }], + + /* + ** Customize the progress-bar color + */ + loading: { color: '#fff' }, + + /* + ** Global CSS + */ + css: [ + 'element-ui/lib/theme-chalk/index.css', + 'bootstrap/dist/css/bootstrap.css', + 'bootstrap-vue/dist/bootstrap-vue.css', + 'v-calendar/lib/v-calendar.min.css' + ], + + /* + ** Plugins to load before mounting the App + */ + plugins: ['@/plugins/element-ui', '@/plugins/filters', + '@/plugins/i18n', '@/plugins/bootstrap-vue', + '@/plugins/vue-awesome', + { src: '@/plugins/v-calendar', ssr: false }, + '@/plugins/magic-grid'], + + /* + ** Nuxt.js modules + */ + modules: [ + // Doc: https://axios.nuxtjs.org/usage + '@nuxtjs/axios' + ], + /* + ** Axios module configuration + */ + axios: { + // See https://github.com/nuxt-community/axios-module#options + }, + + /* + ** Build configuration + */ + build: { + transpile: [/^element-ui/, /^vue-awesome/, /^vue-magic-grid/], + + /* + ** You can extend webpack config here + */ + extend(config, ctx) { + // Run ESLint on save + // if (ctx.isDev && ctx.isClient) { + // config.module.rules.push({ + // enforce: 'pre', + // test: /\.(js|vue)$/, + // loader: 'eslint-loader', + // exclude: /(node_modules)/ + // }) + // } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..08665b1d --- /dev/null +++ b/package.json @@ -0,0 +1,58 @@ +{ + "name": "gancio", + "version": "1.0.0", + "description": "My well-made Nuxt.js project", + "author": "lesion", + "private": true, + "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" + }, + "dependencies": { + "@nuxtjs/axios": "^5.3.6", + "axios": "^0.18.0", + "bcrypt": "^3.0.5", + "bootstrap-vue": "^2.0.0-rc.16", + "cors": "^2.8.5", + "cross-env": "^5.2.0", + "dayjs": "^1.8.11", + "element-ui": "^2.4.11", + "email-templates": "^5.0.4", + "express": "^4.16.4", + "ics": "^2.13.2", + "jsonwebtoken": "^8.5.1", + "mastodon-api": "^1.3.0", + "multer": "^1.4.1", + "nuxt": "^2.4.0", + "sequelize": "^5.2.1", + "sharp": "^0.22.0", + "sqlite3": "^4.0.6", + "v-calendar": "^0.9.7", + "vue-awesome": "^3.5.1", + "vue-i18n": "^8.10.0", + "vue-magic-grid": "^0.0.4" + }, + "devDependencies": { + "@nuxtjs/eslint-config": "^0.0.1", + "babel-eslint": "^10.0.1", + "eslint": "^5.15.1", + "eslint-config-prettier": "^4.1.0", + "eslint-config-standard": ">=12.0.0", + "eslint-loader": "^2.1.2", + "eslint-plugin-import": ">=2.16.0", + "eslint-plugin-jest": ">=22.3.0", + "eslint-plugin-node": ">=8.0.1", + "eslint-plugin-nuxt": ">=0.4.2", + "eslint-plugin-prettier": "^3.0.1", + "eslint-plugin-promise": ">=4.0.1", + "eslint-plugin-standard": ">=4.0.0", + "eslint-plugin-vue": "^5.2.2", + "nodemon": "^1.18.9", + "prettier": "^1.16.4", + "pug-plain-loader": "^1.0.0" + } +} diff --git a/pages/Login.vue b/pages/Login.vue new file mode 100644 index 00000000..6c36308a --- /dev/null +++ b/pages/Login.vue @@ -0,0 +1,65 @@ + + + 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/Register.vue b/pages/Register.vue new file mode 100644 index 00000000..ce0afef8 --- /dev/null +++ b/pages/Register.vue @@ -0,0 +1,60 @@ + + + diff --git a/pages/event/_id.vue b/pages/event/_id.vue new file mode 100644 index 00000000..f7e1aa6f --- /dev/null +++ b/pages/event/_id.vue @@ -0,0 +1,121 @@ + + + + diff --git a/pages/index.vue b/pages/index.vue new file mode 100644 index 00000000..28d95324 --- /dev/null +++ b/pages/index.vue @@ -0,0 +1,2 @@ + diff --git a/pages/new_event.vue b/pages/new_event.vue new file mode 100644 index 00000000..d5be35cb --- /dev/null +++ b/pages/new_event.vue @@ -0,0 +1,215 @@ + + \ No newline at end of file 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/api.js b/plugins/api.js new file mode 100644 index 00000000..72a7dc82 --- /dev/null +++ b/plugins/api.js @@ -0,0 +1,82 @@ +import axios from 'axios' +import { getters } from '@/store' +const api = axios.create({ + baseURL: process.env.NODE_ENV === 'development' ? 'http://localhost:3000/api' : '/api', + withCredentials: true, + responseType: 'json', + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Credentials': 'true', + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } +}) + +function get(path) { + return api.get(path) //, { headers: { 'Authorization': getters.token } }) + .then(res => res.data) + // .catch((e) => { + // if (e.response.status === 403) { + // // store.commit('logout') // TOFIX + // return false + // } + // throw e.response && e.response.data && + // e.response.data.errors && e.response.data.errors[0].message + // }) +} + +function post(path, data) { + return api.post(path, data, { headers: { 'Authorization': getters.token } }) + .then(res => res.data) + // .catch((e) => { + // if (e.response.status === 403) { + // // store.commit('logout') // TOFIX + // return false + // } + // throw e.response && e.response.data && + // e.response.data.errors && e.response.data.errors[0].message + // }) +} +function put(path, data) { + return api.put(path, data, { headers: { 'Authorization': getters.token } }) + .then(ret => ret.data) +} + +function del(path) { + return api.delete(path, { headers: { 'Authorization': getters.token } }).then(ret => ret.data) +} + +export default { + login: (email, password) => post('/login', { email, password }), + register: user => post('/user', user), + + // password recovery + forgotPassword: email => post('/user/recover', { email }), + checkRecoverCode: recover_code => post('/user/check_recover_code', { recover_code }), + recoverPassword: (recover_code, password) => post('/user/recover_password', { recover_code, password }), + + getAllEvents: (month, year) => get(`/event/${year}/${month}/`), + getUnconfirmedEvents: () => get('/event/unconfirmed'), + + confirmEvent: id => get(`/event/confirm/${id}`), + unconfirmEvent: id => get(`/event/unconfirm/${id}`), + + addNotification: notification => post('/event/notification', notification), + delNotification: code => del(`/event/notification/${code}`), + + addEvent: event => post('/user/event', event), + updateEvent: event => put('/user/event', event), + + updatePlace: place => put('/place', place), + delEvent: eventId => del(`/user/event/${eventId}`), + getEvent: eventId => get(`/event/${eventId}`), + getMeta: () => get('/event/meta'), + getUser: () => get('/user'), + getUsers: () => get('/users'), + updateTag: tag => put('/tag', tag), + updateUser: user => put('/user', user), + getAuthURL: mastodonInstance => post('/user/getauthurl', mastodonInstance), + setCode: code => post('/user/code', code), + getAdminSettings: () => get('/settings') + // setAdminSetting: (key, value) => post('/settings', { key, value }) +} diff --git a/plugins/bootstrap-vue.js b/plugins/bootstrap-vue.js new file mode 100644 index 00000000..24f906d9 --- /dev/null +++ b/plugins/bootstrap-vue.js @@ -0,0 +1,6 @@ +import Vue from 'vue' +import BootstrapVue from 'bootstrap-vue' + +export default () => { + Vue.use(BootstrapVue) +} diff --git a/plugins/element-ui.js b/plugins/element-ui.js new file mode 100644 index 00000000..53fc4940 --- /dev/null +++ b/plugins/element-ui.js @@ -0,0 +1,26 @@ +import Vue from 'vue' +import { Button, Select, Tag, Option, Table, FormItem, Card, + Form, Tabs, TabPane, Switch, Input, Loading, TimeSelect, + TableColumn, ColorPicker, Pagination, Popover } from 'element-ui' +// import locale from 'element-ui/lib/locale/lang/en' + +export default () => { + Vue.use(Button) + 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..3e317fc8 --- /dev/null +++ b/plugins/filters.js @@ -0,0 +1,8 @@ +import Vue from 'vue' +import moment from 'dayjs' +import 'dayjs/locale/it' +moment.locale('it') + +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')) diff --git a/plugins/i18n.js b/plugins/i18n.js new file mode 100644 index 00000000..af35c413 --- /dev/null +++ b/plugins/i18n.js @@ -0,0 +1,25 @@ +import Vue from 'vue' +import VueI18n from 'vue-i18n' + +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: 'en' + // messages: { + // 'en': require('~/locales/en.json'), + // 'fr': require('~/locales/fr.json') + // } + }) + + app.i18n.path = (link) => { + if (app.i18n.locale === app.i18n.fallbackLocale) { + return `/${link}` + } + + return `/${app.i18n.locale}/${link}` + } +} diff --git a/plugins/magic-grid.js b/plugins/magic-grid.js new file mode 100644 index 00000000..c240181d --- /dev/null +++ b/plugins/magic-grid.js @@ -0,0 +1,6 @@ +import Vue from 'vue' +import MagicGrid from 'vue-magic-grid' + +export default () => { + Vue.use(MagicGrid) +} diff --git a/plugins/v-calendar.js b/plugins/v-calendar.js new file mode 100644 index 00000000..fd7f3431 --- /dev/null +++ b/plugins/v-calendar.js @@ -0,0 +1,9 @@ +import Vue from 'vue' +import VCalendar from 'v-calendar' +import 'v-calendar/lib/v-calendar.min.css' + +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..333743f7 --- /dev/null +++ b/plugins/vue-awesome.js @@ -0,0 +1,24 @@ +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 Icon from 'vue-awesome/components/Icon' + +export default () => { + Vue.component('v-icon', Icon) +} diff --git a/server/api/auth.js b/server/api/auth.js new file mode 100644 index 00000000..0727af5d --- /dev/null +++ b/server/api/auth.js @@ -0,0 +1,48 @@ +const jwt = require('jsonwebtoken') +const { Op } = require('sequelize') +const config = require('./config') +const User = require('./models/user') + +const Auth = { + fillUser(req, res, next) { + const token = + req.body.token || req.params.token || req.headers['x-access-token'] + if (!token) return next() + jwt.verify(token, config.secret, async (err, decoded) => { + if (err) return next() + req.user = await User.findOne({ + where: { email: { [Op.eq]: decoded.email }, is_active: true } + }) + next() + }) + }, + isAuth(req, res, next) { + const token = + (req.body && req.body.token) || + req.params.token || + req.headers['x-access-token'] + if (!token) return res.status(403).send({ message: 'Token not found' }) + jwt.verify(token, config.secret, async (err, decoded) => { + if (err) { + return res + .status(403) + .send({ message: 'Failed to authenticate token ' + err }) + } + req.user = await User.findOne({ + where: { email: { [Op.eq]: decoded.email }, is_active: true } + }) + if (!req.user) { + return res + .status(403) + .send({ message: 'Failed to authenticate token ' + err }) + } + next() + }) + }, + isAdmin(req, res, next) { + if (req.user.is_admin && req.user.is_active) return next() + return res.status(403).send({ message: 'Admin needed' }) + } +} + +module.exports = Auth diff --git a/server/api/config.js b/server/api/config.js new file mode 100644 index 00000000..e0f2b99f --- /dev/null +++ b/server/api/config.js @@ -0,0 +1,27 @@ +/* backend configuration */ +const env = process.env.NODE_ENV || 'development' +const db = require('./config/config.json')[env] + +module.exports = { + locale: process.env.LOCALE || 'it', + title: process.env.TITLE || 'GANCIO', + description: process.env.DESCRIPTION || 'A calendar for radical communities', + baseurl: process.env.BASE_URL || 'http://localhost:8080', + apiurl: + env === 'production' + ? process.env.BASE_URL + '/api' + : 'http://localhost:9000', + db, + admin: process.env.ADMIN_EMAIL, + + smtp: { + host: process.env.SMTP_HOST, + secure: true, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + } + }, + + secret: process.env.SECRET || 'notsosecret' +} diff --git a/server/api/config/config.json b/server/api/config/config.json new file mode 100644 index 00000000..b0b61bda --- /dev/null +++ b/server/api/config/config.json @@ -0,0 +1,14 @@ +{ + "development": { + "storage": "/home/les/dev/hacklab/gancio/db.sqlite", + "dialect": "sqlite", + "logging": false + }, + "production": { + "username": "docker", + "password": "docker", + "database": "gancio", + "host": "db", + "dialect": "postgres" + } +} diff --git a/server/api/controller/bot.js b/server/api/controller/bot.js new file mode 100644 index 00000000..bedcae91 --- /dev/null +++ b/server/api/controller/bot.js @@ -0,0 +1,82 @@ +// const { User, Event, Comment, Tag } = require('../model') +const config = require('../config') +const Mastodon = require('mastodon-api') +// const Sequelize = require('sequelize') +// const Op = Sequelize.Op +const fs = require('fs') +const path = require('path') +const moment = require('moment') +moment.locale('it') + +const botController = { + bots: [], + // async initialize () { + // console.log('initialize bots') + // const botUsers = await User.findAll({ where: { mastodon_auth: { [Op.ne]: null } } }) + // console.log(botUsers) + // botController.bots = botUsers.map(user => { + // console.log('initialize bot ', user.name) + // console.log('.. ', user.mastodon_auth) + // const { client_id, client_secret, access_token } = user.mastodon_auth + // const bot = new Mastodon({ access_token, api_url: `https://${user.mastodon_instance}/api/v1/` }) + // const listener = bot.stream('streaming/direct') + // listener.on('message', botController.message) + // listener.on('error', botController.error) + // return { email: user.email, bot } + // }) + // console.log(botController.bots) + // }, + // add (user, token) { + // const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.mastodon_instance}/api/v1/` }) + // const listener = bot.stream('streaming/direct') + // listener.on('message', botController.message) + // listener.on('error', botController.error) + // botController.bots.push({ email: user.email, bot }) + // }, + async post (mastodon_auth, event) { + const { access_token, instance } = mastodon_auth + const bot = new Mastodon({ access_token, api_url: `https://${instance}/api/v1/` }) + const status = `${event.title} @ ${event.place.name} ${moment(event.start_datetime).format('ddd, D MMMM HH:mm')} - +${event.description.length > 200 ? event.description.substr(0, 200) + '...' : event.description} - ${event.tags.map(t => '#' + t.tag).join(' ')} ${config.baseurl}/event/${event.id}` + + let media + if (event.image_path) { + const file = path.join(__dirname, '..', '..', 'uploads', event.image_path) + if (fs.statSync(file)) { + media = await bot.post('media', { file: fs.createReadStream(file) }) + } + } + return bot.post('statuses', { status, visibility: 'public', media_ids: media ? [media.data.id] : [] }) + } + // async message (msg) { + // console.log(msg) + // console.log(msg.data.accounts) + // const replyid = msg.data.in_reply_to_id || msg.data.last_status.in_reply_to_id + // if (!replyid) return + // const event = await Event.findOne({ where: { activitypub_id: replyid } }) + // if (!event) { + // check for comment.. + // const comment = await Comment.findOne( {where: { }}) + // } + // const comment = await Comment.create({activitypub_id: msg.data.last_status.id, text: msg.data.last_status.content, author: msg.data.accounts[0].username }) + // event.addComment(comment) + // console.log(event) + // const comment = await Comment.findOne( { where: {activitypub_id: msg.data.in_reply_to}} ) + // console.log('dentro message ', data) + + // add comment to specified event + // let comment + // if (!event) { + // const comment = await Comment.findOne({where: {activitypub_id: req.body.id}, include: Event}) + // event = comment.event + // } + // const comment = new Comment(req.body) + // event.addComment(comment) + // }, + // error (err) { + // console.log('error ', err) + // } +} + +// setTimeout(botController.initialize, 2000) +module.exports = botController diff --git a/server/api/controller/event.js b/server/api/controller/event.js new file mode 100644 index 00000000..d1c97801 --- /dev/null +++ b/server/api/controller/event.js @@ -0,0 +1,169 @@ +const crypto = require('crypto') +const moment = require('moment') +const { Op } = require('sequelize') +const lodash = require('lodash') +const { User, Event, Comment, Tag, Place, Notification } = require('../model') + +const eventController = { + + async addComment(req, res) { + // comment could be added to an event or to another comment + let event = await Event.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } } }) + if (!event) { + const comment = await Comment.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } }, include: Event }) + event = comment.event + } + const comment = new Comment(req.body) + event.addComment(comment) + res.json(comment) + }, + + async getMeta(req, res) { + const places = await Place.findAll() + const tags = await Tag.findAll() + res.json({ tags, places }) + }, + + async getNotifications(event) { + function match(event, filters) { + // matches if no filter specified + if (!filters) return true + + // check for visibility + if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) return false + + if (!filters.tags && !filters.places) return true + if (!filters.tags.length && !filters.places.length) return true + if (filters.tags.length) { + const m = lodash.intersection(event.tags.map(t => t.tag), filters.tags) + if (m.length > 0) return true + } + if (filters.places.length) { + if (filters.places.find(p => p === event.place.name)) { + return true + } + } + } + const notifications = await Notification.findAll() + + // get notification that matches with selected event + return notifications.filter(notification => match(event, notification.filters)) + }, + + async updateTag(req, res) { + const tag = await Tag.findByPk(req.body.tag) + if (tag) { + res.json(await tag.update(req.body)) + } else { + res.sendStatus(404) + } + }, + + async updatePlace(req, res) { + const place = await Place.findByPk(req.body.id) + await place.update(req.body) + res.json(place) + }, + + async get(req, res) { + const id = req.params.event_id + const event = await Event.findByPk(id, { include: [User, Tag, Comment, Place] }) + res.json(event) + }, + + async confirm(req, res) { + const id = req.params.event_id + const event = await Event.findByPk(id) + + try { + await event.update({ is_visible: true }) + // insert notification + const notifications = await eventController.getNotifications(event) + await event.setNotifications(notifications) + res.sendStatus(200) + } catch (e) { + res.sendStatus(404) + } + }, + + async unconfirm(req, res) { + const id = req.params.event_id + const event = await Event.findByPk(id) + + try { + await event.update({ is_visible: false }) + res.sendStatus(200) + } catch (e) { + res.sendStatus(404) + } + }, + + async getUnconfirmed(req, res) { + const events = await Event.findAll({ + where: { + is_visible: false + }, + order: [['start_datetime', 'ASC']], + include: [Tag, Place] + }) + res.json(events) + }, + + async addNotification(req, res) { + try { + const notification = { + filters: { is_visible: true }, + email: req.body.email, + type: 'mail', + remove_code: crypto.randomBytes(16).toString('hex') + } + await Notification.create(notification) + res.sendStatus(200) + } catch (e) { + res.sendStatus(404) + } + }, + + async delNotification(req, res) { + const remove_code = req.params.code + try { + const notification = await Notification.findOne({ where: { remove_code: { [Op.eq]: remove_code } } }) + await notification.destroy() + } catch (e) { + return res.sendStatus(404) + } + res.sendStatus(200) + }, + + async getAll(req, res) { + console.log('sono qui dentro !') + // this is due how v-calendar shows dates + const start = moment().year(req.params.year).month(req.params.month) + .startOf('month').startOf('isoWeek') + let end = moment().year(req.params.year).month(req.params.month).endOf('month') + const shownDays = end.diff(start, 'days') + if (shownDays <= 34) end = end.add(1, 'week') + end = end.endOf('isoWeek') + const events = await Event.findAll({ + // where: { + // is_visible: true, + // [Op.and]: [ + // { start_datetime: { [Op.gte]: start } }, + // { start_datetime: { [Op.lte]: end } } + // ] + // }, + // order: [['start_datetime', 'ASC']], + // include: [ + // { model: User, required: false }, + // Comment, + // Tag, + // { model: Place, required: false } + // ] + }) + console.log(events) + res.json(events) + } + +} + +module.exports = eventController diff --git a/server/api/controller/export.js b/server/api/controller/export.js new file mode 100644 index 00000000..3f54ecf2 --- /dev/null +++ b/server/api/controller/export.js @@ -0,0 +1,64 @@ +const { Event, Comment, Tag, Place } = require('../model') +const { Op } = require('sequelize') +const config = require('../config') +const moment = require('moment') +const ics = require('ics') + +const exportController = { + + async export (req, res) { + console.log('type ', req.params.type) + const type = req.params.type + const tags = req.query.tags + const places = req.query.places + const whereTag = {} + const wherePlace = {} + const yesterday = moment().subtract('1', 'day') + if (tags) { + whereTag.tag = tags.split(',') + } + if (places) { + wherePlace.name = places.split(',') + } + const events = await Event.findAll({ + where: { is_visible: true, start_datetime: { [Op.gte]: yesterday } }, + include: [Comment, { + model: Tag, + where: whereTag + }, { model: Place, where: wherePlace } ] + }) + switch (type) { + case 'feed': + return exportController.feed(res, events.slice(0, 20)) + case 'ics': + return exportController.ics(res, events) + } + }, + + async feed (res, events) { + res.type('application/rss+xml; charset=UTF-8') + res.render('feed/rss.pug', { events, config, moment }) + }, + + async ics (res, events) { + const eventsMap = events.map(e => { + const tmpStart = moment(e.start_datetime) + const tmpEnd = moment(e.end_datetime) + const start = [tmpStart.year(), tmpStart.month() + 1, tmpStart.date(), tmpStart.hour(), tmpStart.minute()] + const end = [tmpEnd.year(), tmpEnd.month() + 1, tmpEnd.date(), tmpEnd.hour(), tmpEnd.minute()] + return { + start, + end, + title: e.title, + description: e.description, + location: e.place.name + ' ' + e.place.address + } + }) + res.type('text/calendar; charset=UTF-8') + const { error, value } = ics.createEvents(eventsMap) + console.log(error, value) + res.send(value) + } +} + +module.exports = exportController diff --git a/server/api/controller/settings.js b/server/api/controller/settings.js new file mode 100644 index 00000000..85f32819 --- /dev/null +++ b/server/api/controller/settings.js @@ -0,0 +1,27 @@ +const { Settings } = require('../model') + +const settingsController = { + async setAdminSetting (key, value) { + await Settings.findOrCreate({ where: { key }, + defaults: { value } }) + .spread((settings, created) => { + if (!created) return settings.update({ value }) + }) + }, + + async getAdminSettings (req, res) { + const settings = await settingsController.settings() + res.json(settings) + }, + + async settings () { + const settings = await Settings.findAll() + const map = {} + settings.forEach(setting => { + map[setting.key] = setting.value + }) + return map + } +} + +module.exports = settingsController diff --git a/server/api/controller/user.js b/server/api/controller/user.js new file mode 100644 index 00000000..923e2414 --- /dev/null +++ b/server/api/controller/user.js @@ -0,0 +1,283 @@ +const jwt = require('jsonwebtoken') +const Mastodon = require('mastodon-api') + +const User = require('../models/user') +const { Event, Tag, Place } = require('../models/event') +const settingsController = require('./settings') +const eventController = require('./event') +const config = require('../config') +const mail = require('../mail') +const { Op } = require('sequelize') +const fs = require('fs') +const path = require('path') +const crypto = require('crypto') + +const userController = { + async login (req, res) { + // find the user + const user = await User.findOne({ where: { email: { [Op.eq]: req.body.email } } }) + if (!user) { + res.status(404).json({ success: false, message: 'AUTH_FAIL' }) + } else if (user) { + if (!user.is_active) { + res.status(403).json({ success: false, message: 'NOT_CONFIRMED' }) + // check if password matches + } else if (!await user.comparePassword(req.body.password)) { + res.status(403).json({ success: false, message: 'AUTH_FAIL' }) + } else { + // if user is found and password is right + // create a token + const payload = { email: user.email } + var token = jwt.sign(payload, config.secret) + res.json({ + success: true, + message: 'Enjoy your token!', + token, + user + }) + } + } + }, + + async setToken (req, res) { + req.user.mastodon_auth = req.body + await req.user.save() + res.json(req.user) + }, + + async delEvent (req, res) { + const event = await Event.findByPk(req.params.id) + // check if event is mine (or user is admin) + if (event && (req.user.is_admin || req.user.id === event.userId)) { + if (event.image_path) { + const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path) + const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path) + await fs.unlink(old_path) + await fs.unlink(old_thumb_path) + } + await event.destroy() + res.sendStatus(200) + } else { + res.sendStatus(403) + } + }, + + // ADD EVENT + async addEvent (req, res) { + const body = req.body + + // remove description tag and create anchor tags + const description = body.description + .replace(/(<([^>]+)>)/ig, '') + .replace(/(https?:\/\/[^\s]+)/g, '$1') + + const eventDetails = { + title: body.title, + description, + multidate: body.multidate, + start_datetime: body.start_datetime, + end_datetime: body.end_datetime, + is_visible: !!req.user + } + + if (req.file) { + eventDetails.image_path = req.file.filename + } + + let event = await Event.create(eventDetails) + + // create place + let place + try { + place = await Place.findOrCreate({ where: { name: body.place_name }, + defaults: { address: body.place_address } }) + .spread((place, created) => place) + await event.setPlace(place) + } catch (e) { + console.error(e) + } + + // create/assign tags + if (body.tags) { + await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true }) + const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } }) + await event.addTags(tags) + } + if (req.user) await req.user.addEvent(event) + event = await Event.findByPk(event.id, { include: [User, Tag, Place] }) + + // insert notifications + const notifications = await eventController.getNotifications(event) + await event.setNotifications(notifications) + + return res.json(event) + }, + + async updateEvent (req, res) { + const body = req.body + const event = await Event.findByPk(body.id) + if (!req.user.is_admin && event.userId !== req.user.id) { + return res.sendStatus(403) + } + + if (req.file) { + if (event.image_path) { + const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path) + const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path) + await fs.unlink(old_path, e => console.error(e)) + await fs.unlink(old_thumb_path, e => console.error(e)) + } + body.image_path = req.file.filename + } + + body.description = body.description + .replace(/(<([^>]+)>)/ig, '') // remove all tags from description + .replace(/(https?:\/\/[^\s]+)/g, '$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 getAuthURL (req, res) { + const instance = req.body.instance + const is_admin = req.body.admin && req.user.is_admin + const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}` + const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`, + config.title, 'read write', callback) + const url = await Mastodon.getAuthorizationUrl(client_id, client_secret, + `https://${instance}`, 'read write', callback) + + if (is_admin) { + await settingsController.setAdminSetting('mastodon_auth', { client_id, client_secret, instance }) + } else { + req.user.mastodon_auth = { client_id, client_secret, instance } + await req.user.save() + } + res.json(url) + }, + + async code (req, res) { + const { code, is_admin } = req.body + let client_id, client_secret, instance + const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}` + + if (is_admin) { + const settings = await settingsController.settings(); + ({ client_id, client_secret, instance } = settings.mastodon_auth) + } else { + ({ client_id, client_secret, instance } = req.user.mastodon_auth) + } + + try { + const token = await Mastodon.getAccessToken(client_id, client_secret, code, + `https://${instance}`, callback) + const mastodon_auth = { client_id, client_secret, access_token: token, instance } + if (is_admin) { + await settingsController.setAdminSetting('mastodon_auth', mastodon_auth) + res.json(instance) + } else { + req.user.mastodon_auth = mastodon_auth + await req.user.save() + // await bot.add(req.user, token) + res.json(req.user) + } + } catch (e) { + res.json(e) + } + }, + + async forgotPassword (req, res) { + const email = req.body.email + const user = await User.findOne({ where: { email: { [Op.eq]: email } } }) + if (!user) return res.sendStatus(200) + + user.recover_code = crypto.randomBytes(16).toString('hex') + mail.send(user.email, 'recover', { user, config }) + await user.save() + res.sendStatus(200) + }, + + async checkRecoverCode (req, res) { + const recover_code = req.body.recover_code + if (!recover_code) return res.sendStatus(400) + const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } }) + if (!user) return res.sendStatus(400) + res.json(user) + }, + + async updatePasswordWithRecoverCode (req, res) { + const recover_code = req.body.recover_code + if (!recover_code) return res.sendStatus(400) + const password = req.body.password + const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } }) + if (!user) return res.sendStatus(400) + user.password = password + await user.save() + res.sendStatus(200) + }, + + async current (req, res) { + res.json(req.user) + }, + + async getAll (req, res) { + const users = await User.findAll({ + order: [['createdAt', 'DESC']] + }) + res.json(users) + }, + + async update (req, res) { + const user = await User.findByPk(req.body.id) + if (user) { + if (!user.is_active && req.body.is_active) { + await mail.send(user.email, 'confirm', { user, config }) + } + await user.update(req.body) + res.json(user) + } else { + res.sendStatus(400) + } + }, + + async register (req, res) { + const n_users = await User.count() + try { + if (n_users === 0) { + // the first registered user will be an active admin + req.body.is_active = req.body.is_admin = true + } else { + req.body.is_active = false + } + const user = await User.create(req.body) + try { + mail.send([user.email, config.admin], 'register', { user, config }) + } catch (e) { + return res.status(400).json(e) + } + const payload = { email: user.email } + const token = jwt.sign(payload, config.secret) + res.json({ user, token }) + } catch (e) { + res.status(404).json(e) + } + } +} + +module.exports = userController diff --git a/server/api/db.js b/server/api/db.js new file mode 100644 index 00000000..7de7ec67 --- /dev/null +++ b/server/api/db.js @@ -0,0 +1,5 @@ +const Sequelize = require('sequelize') +const conf = require('./config.js') +const db = new Sequelize(conf.db) +// db.sync({force: true}) +module.exports = db diff --git a/server/api/index.js b/server/api/index.js new file mode 100644 index 00000000..ebf7fb8b --- /dev/null +++ b/server/api/index.js @@ -0,0 +1,86 @@ +const express = require('express') +const multer = require('multer') +const { fillUser, isAuth, isAdmin } = require('./auth') +const eventController = require('./controller/event') +const exportController = require('./controller/export') +const userController = require('./controller/user') +const settingsController = require('./controller/settings') + +// const botController = require('./controller/bot') + +const storage = require('./storage')({ + destination: 'uploads/' +}) + +const upload = multer({ storage }) +const api = express.Router() +// login +api.post('/login', userController.login) +api.post('/user/recover', userController.forgotPassword) +api.post('/user/check_recover_code', userController.checkRecoverCode) +api.post('/user/recover_password', userController.updatePasswordWithRecoverCode) + +api + .route('/user') + // register + .post(userController.register) + // get current user + .get(isAuth, userController.current) + // update user (eg. confirm) + .put(isAuth, isAdmin, userController.update) + +// get all users +api.get('/users', isAuth, isAdmin, userController.getAll) + +// update a tag (modify color) +api.put('/tag', isAuth, isAdmin, eventController.updateTag) + +// update a place (modify address..) +api.put('/place', isAuth, isAdmin, eventController.updatePlace) + +api + .route('/user/event') + // add event + .post(fillUser, upload.single('image'), userController.addEvent) + // update event + .put(isAuth, upload.single('image'), userController.updateEvent) + +// remove event +api.delete('/user/event/:id', isAuth, userController.delEvent) + +// get tags/places +api.get('/event/meta', eventController.getMeta) + +// get unconfirmed events +api.get('/event/unconfirmed', isAuth, isAdmin, eventController.getUnconfirmed) + +// add event notification +api.post('/event/notification', eventController.addNotification) +api.delete('/event/notification/:code', eventController.delNotification) + +api.get('/settings', settingsController.getAdminSettings) +api.post('/settings', settingsController.setAdminSetting) + +// get event +api.get('/event/:event_id', eventController.get) + +// confirm event +api.get('/event/confirm/:event_id', isAuth, isAdmin, eventController.confirm) +api.get( + '/event/unconfirm/:event_id', + isAuth, + isAdmin, + eventController.unconfirm +) + +// export events (rss/ics) +api.get('/export/:type', exportController.export) + +// get events in this range +api.get('/event/:year/:month', eventController.getAll) + +// mastodon oauth auth +api.post('/user/getauthurl', isAuth, userController.getAuthURL) +api.post('/user/code', isAuth, userController.code) + +module.exports = api diff --git a/server/api/mail.js b/server/api/mail.js new file mode 100644 index 00000000..4f38ee11 --- /dev/null +++ b/server/api/mail.js @@ -0,0 +1,44 @@ +const Email = require('email-templates') +const path = require('path') +const config = require('./config') +const moment = require('moment') +moment.locale('it') + +const mail = { + send (addresses, template, locals) { + const email = new Email({ + views: { root: path.join(__dirname, 'emails') }, + juice: true, + juiceResources: { + preserveImportant: true, + webResources: { + relativeTo: path.join(__dirname, 'emails') + } + }, + message: { + from: `${config.title} <${config.smtp.auth.user}>` + }, + send: true, + i18n: { + locales: ['en', 'es', 'it'], + defaultLocale: config.locale + }, + transport: config.smtp + }) + return email.send({ + template, + message: { + to: addresses, + bcc: config.admin + }, + locals: { + ...locals, + locale: config.locale, + config, + datetime: datetime => moment(datetime).format('ddd, D MMMM HH:mm') + } + }) + } +} + +module.exports = mail diff --git a/server/api/model.js b/server/api/model.js new file mode 100644 index 00000000..303fa1dd --- /dev/null +++ b/server/api/model.js @@ -0,0 +1,14 @@ +const User = require('./models/user') +const { Event, Comment, Tag, Place, Notification, EventNotification } = require('./models/event') +const Settings = require('./models/settings') + +module.exports = { + User, + Event, + Comment, + Tag, + Place, + Notification, + EventNotification, + Settings +} diff --git a/server/api/models/event.js b/server/api/models/event.js new file mode 100644 index 00000000..35524b74 --- /dev/null +++ b/server/api/models/event.js @@ -0,0 +1,72 @@ +const Sequelize = require('sequelize') +const db = require('../db') +const User = require('./user') + +const Event = db.define('event', { + title: Sequelize.STRING, + description: Sequelize.TEXT, + multidate: Sequelize.BOOLEAN, + start_datetime: { type: Sequelize.DATE, index: true }, + end_datetime: { type: Sequelize.DATE, index: true }, + image_path: Sequelize.STRING, + activitypub_id: { type: Sequelize.INTEGER, index: true }, + is_visible: Sequelize.BOOLEAN +}) + +const Tag = db.define('tag', { + tag: { type: Sequelize.STRING, index: true, unique: true, primaryKey: true }, + color: { type: Sequelize.STRING } +}) + +const Comment = db.define('comment', { + activitypub_id: { type: Sequelize.INTEGER, index: true }, + author: Sequelize.STRING, + text: Sequelize.STRING +}) + +const Notification = db.define('notification', { + filters: Sequelize.JSON, + email: Sequelize.STRING, + remove_code: Sequelize.STRING, + type: { + type: Sequelize.ENUM, + values: ['mail', 'admin_email', 'mastodon'] + } +}) + +const Place = db.define('place', { + name: { type: Sequelize.STRING, unique: true, index: true }, + address: { type: Sequelize.STRING } +}) + +Comment.belongsTo(Event) +Event.hasMany(Comment) + +Event.belongsToMany(Tag, { through: 'tagEvent' }) +Tag.belongsToMany(Event, { through: 'tagEvent' }) + +const EventNotification = db.define('EventNotification', { + status: { + type: Sequelize.ENUM, + values: ['new', 'sent', 'error'], + defaultValue: 'new', + index: true + } +}) + +Event.belongsToMany(Notification, { through: EventNotification }) +Notification.belongsToMany(Event, { through: EventNotification }) + +Event.belongsTo(User) +Event.belongsTo(Place) + +User.hasMany(Event) +Place.hasMany(Event) + +// async function init() { +// await Notification.findOrCreate({ where: { type: 'mastodon', filters: { is_visible: true } } }) +// await Notification.findOrCreate({ where: { type: 'admin_email', filters: { is_visible: false } } }) +// } + +// init() +module.exports = { Event, Comment, Tag, Place, Notification, EventNotification } diff --git a/server/api/models/index.js b/server/api/models/index.js new file mode 100644 index 00000000..c1a3d6d5 --- /dev/null +++ b/server/api/models/index.js @@ -0,0 +1,37 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'development'; +const config = require(__dirname + '/../config/config.json')[env]; +const db = {}; + +let sequelize; +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + sequelize = new Sequelize(config.database, config.username, config.password, config); +} + +fs + .readdirSync(__dirname) + .filter(file => { + return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); + }) + .forEach(file => { + const model = sequelize['import'](path.join(__dirname, file)); + db[model.name] = model; + }); + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/server/api/models/settings.js b/server/api/models/settings.js new file mode 100644 index 00000000..148b7bdb --- /dev/null +++ b/server/api/models/settings.js @@ -0,0 +1,9 @@ +const db = require('../db') +const Sequelize = require('sequelize') + +const Settings = db.define('settings', { + key: { type: Sequelize.STRING, primaryKey: true, index: true }, + value: Sequelize.JSON +}) + +module.exports = Settings diff --git a/server/api/models/user.js b/server/api/models/user.js new file mode 100644 index 00000000..2fcfbac5 --- /dev/null +++ b/server/api/models/user.js @@ -0,0 +1,34 @@ +const bcrypt = require('bcrypt') +const db = require('../db') +const Sequelize = require('sequelize') + +const User = db.define('user', { + email: { + type: Sequelize.STRING, + unique: { msg: 'Email already exists' }, + index: true, + allowNull: false + }, + description: Sequelize.TEXT, + password: Sequelize.STRING, + recover_code: Sequelize.STRING, + is_admin: Sequelize.BOOLEAN, + is_active: Sequelize.BOOLEAN, + mastodon_auth: Sequelize.JSON +}) + +User.prototype.comparePassword = async function (pwd) { + if (!this.password) return false + const ret = await bcrypt.compare(pwd, this.password) + return ret +} + +User.beforeSave(async (user, options) => { + if (user.changed('password')) { + const salt = await bcrypt.genSalt(10) + const hash = await bcrypt.hash(user.password, salt) + user.password = hash + } +}) + +module.exports = User diff --git a/server/api/storage.js b/server/api/storage.js new file mode 100644 index 00000000..da58bae4 --- /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: 80 }) + const thumbnailer = sharp().resize(400).jpeg({ quality: 60 }) + + file.stream.pipe(thumbnailer).pipe(thumbStream) + thumbStream.on('error', e => console.log('thumbStream error ', e)) + + file.stream.pipe(resizer).pipe(outStream) + outStream.on('error', cb) + outStream.on('finish', function () { + cb(null, { + destination, + filename, + path: finalPath, + size: outStream.bytesWritten + }) + }) + }) +} + +DiskStorage.prototype._removeFile = function _removeFile(req, file, cb) { + let path = file.path + + delete file.destination + delete file.filename + delete file.path + + fs.unlink(path, cb) +} + +module.exports = function (opts) { + return new DiskStorage(opts) +} diff --git a/server/index.js b/server/index.js new file mode 100644 index 00000000..ebcf3d73 --- /dev/null +++ b/server/index.js @@ -0,0 +1,36 @@ +const express = require('express') +const consola = require('consola') +const { Nuxt, Builder } = require('nuxt') +const app = express() +const cors = require('cors') + +// Import and Set Nuxt.js options +const config = require('../nuxt.config.js') +config.dev = !(process.env.NODE_ENV === 'production') + +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(cors()) + app.use(nuxt.render) + + // Listen the server + app.listen(port, host) + consola.ready({ + message: `Server listening on http://${host}:${port}`, + badge: true + }) +} +start() 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 0000000000000000000000000000000000000000..382fecbbf96d6e1e614e0e2cc8b73e355bd946cc GIT binary patch literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x#lFaYI-8)(_-RMWh}@jndLuXgyK z;A{Fn&J#OMi823Q&|nS5g-td!^Y-RSMpI2!G(|^BVz5@p+ zJll3Uhal^3+~)80K_I1H1Blkn|X#f`-nA@7V7^0XJCNg21cM?f%pJ31V3PB ZU;yC{{0s~~ ({ + events: [], + user: {}, + logged: false, + token: '', + tags: [], + places: [], + filters: { + tags: [], + places: [] + } +}) + +export const getters = { + token: state => state.token, + // filter current + future events only + // plus, filter matches search tag/place + filteredEvents: (state) => { + const events = state.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 + }) + if (!state.filters.tags.length && !state.filters.places.length) { + return events + } + return 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.name)) { + return true + } + } + return 0 + }) + } + +} + +export const mutations = { + logout(state) { + state.logged = false + state.token = '' + state.user = {} + }, + login(state, user) { + state.logged = true + state.user = user.user + state.token = user.token + }, + setEvents(state, events) { + state.events = events + }, + 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 => ev.id !== eventId) + }, + update(state, { tags, places }) { + state.tags = tags + state.places = places + }, + // search + addSearchTag(state, tag) { + if (!state.filters.tags.find(t => t === tag.tag)) { + state.filters.tags.push(tag.tag) + } else { + state.filters.tags = state.filters.tags.filter(t => t !== tag.tag) + } + }, + setSearchTags(state, tags) { + state.filters.tags = tags + }, + addSearchPlace(state, place) { + if (state.filters.places.find(p => p.name === place.name)) { + state.filters.places.push(place) + } + }, + setSearchPlaces(state, places) { + state.filters.places = places + } +} + +export const actions = { + // called on server request + // get current month's event + async nuxtServerInit({ commit }, { req }) { + // set user if logged! TODO + + const now = new Date() + const events = await api.getAllEvents(now.getMonth() - 1, now.getFullYear()) + commit('setEvents', events) + }, + async updateEvents({ commit }, date) { + console.log('dentro updateEvents ', date.month, api) + try { + const events = await api.getAllEvents(date.month - 1, date.year) + console.log('dopo getAll events', events) + commit('setEvents', events) + } catch (e) { + console.log(e) + } + }, + async updateMeta({ commit }) { + const { tags, places } = await api.getMeta() + commit('update', { tags, places }) + }, + async addEvent({ commit }, formData) { + const event = await api.addEvent(formData) + if (this.state.logged) { + commit('addEvent', event) + } + }, + async updateEvent({ commit }, formData) { + const event = await api.updateEvent(formData) + commit('updateEvent', event) + }, + delEvent({ commit }, eventId) { + commit('delEvent', eventId) + }, + login({ commit }, user) { + commit('login', user) + }, + logout({ commit }) { + commit('logout') + }, + // search + addSearchTag({ commit }, tag) { + commit('addSearchTag', tag) + }, + setSearchTags({ commit }, tags) { + commit('setSearchTags', tags) + }, + addSearchPlace({ commit }, place) { + commit('addSearchPlace', place) + }, + setSearchPlaces({ commit }, places) { + commit('setSearchPlaces', places) + } +} +// export const getters = { +// filteredEvents: state => state.events +// }