From afa6ad2a6b3fbb650e3ef9fc7ab48e80b4c93d03 Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 2 Apr 2019 23:23:45 +0200 Subject: [PATCH 01/14] init --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..0ffa0472 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# gancio + +> My well-made Nuxt.js project + +## Build Setup + +``` bash +# install dependencies +$ yarn install + +# serve with hot reload at localhost:3000 +$ yarn run dev + +# build for production and launch server +$ yarn run build +$ yarn start + +# generate static project +$ yarn run generate +``` + +For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org). From 88b43f9bb117376a9b475ba5375efe4199d177a4 Mon Sep 17 00:00:00 2001 From: lesion Date: Wed, 3 Apr 2019 00:25:12 +0200 Subject: [PATCH 02/14] 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 +// } From 35fa25c060a48420d57260ee551ef5acd8ef473d Mon Sep 17 00:00:00 2001 From: lesion Date: Fri, 5 Apr 2019 00:10:19 +0200 Subject: [PATCH 03/14] working on ssr --- package.json | 1 + pages/about.vue | 34 +++++ pages/admin.vue | 196 +++++++++++++++++++++++++++ pages/{Register.vue => register.vue} | 4 +- plugins/api.js | 2 +- server/api/controller/user.js | 53 ++++---- server/index.js | 11 +- 7 files changed, 271 insertions(+), 30 deletions(-) create mode 100644 pages/about.vue create mode 100644 pages/admin.vue rename pages/{Register.vue => register.vue} (95%) diff --git a/package.json b/package.json index 08665b1d..c5211d39 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@nuxtjs/axios": "^5.3.6", "axios": "^0.18.0", "bcrypt": "^3.0.5", + "body-parser": "^1.18.3", "bootstrap-vue": "^2.0.0-rc.16", "cors": "^2.8.5", "cross-env": "^5.2.0", diff --git a/pages/about.vue b/pages/about.vue new file mode 100644 index 00000000..81805c9e --- /dev/null +++ b/pages/about.vue @@ -0,0 +1,34 @@ + \ No newline at end of file diff --git a/pages/admin.vue b/pages/admin.vue new file mode 100644 index 00000000..330b93f4 --- /dev/null +++ b/pages/admin.vue @@ -0,0 +1,196 @@ + + \ No newline at end of file diff --git a/pages/Register.vue b/pages/register.vue similarity index 95% rename from pages/Register.vue rename to pages/register.vue index ce0afef8..86891161 100644 --- a/pages/Register.vue +++ b/pages/register.vue @@ -18,8 +18,8 @@ + + diff --git a/pages/Login.vue b/pages/login.vue similarity index 58% rename from pages/Login.vue rename to pages/login.vue index 6ed16bbe..eac775f2 100644 --- a/pages/Login.vue +++ b/pages/login.vue @@ -1,19 +1,20 @@ + diff --git a/plugins/i18n.js b/plugins/i18n.js index af35c413..c3a14c31 100644 --- a/plugins/i18n.js +++ b/plugins/i18n.js @@ -8,18 +8,18 @@ export default ({ app, store }) => { // 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'), + fallbackLocale: 'it', + messages: { + 'it': require('~/locales/it.json') // 'fr': require('~/locales/fr.json') - // } + } }) - app.i18n.path = (link) => { - if (app.i18n.locale === app.i18n.fallbackLocale) { - return `/${link}` - } + // app.i18n.path = (link) => { + // if (app.i18n.locale === app.i18n.fallbackLocale) { + // return `/${link}` + // } - return `/${app.i18n.locale}/${link}` - } + // return `/${app.i18n.locale}/${link}` + // } } diff --git a/server/api/auth.js b/server/api/auth.js index 9bcffee7..fc74a8b4 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -21,7 +21,6 @@ const Auth = { (req.body && req.body.token) || req.params.token || req.headers.authorization - console.error('sono dentro isAuth ', token, req.headers) if (!token) return res.status(403).send({ message: 'Token not found' }) jwt.verify(token, config.secret, async (err, decoded) => { if (err) { diff --git a/server/api/controller/event.js b/server/api/controller/event.js index d1c97801..d6925fec 100644 --- a/server/api/controller/event.js +++ b/server/api/controller/event.js @@ -160,7 +160,7 @@ const eventController = { // { model: Place, required: false } // ] }) - console.log(events) + // console.log(events) res.json(events) } diff --git a/server/api/controller/user.js b/server/api/controller/user.js index 3e8c1938..432f5813 100644 --- a/server/api/controller/user.js +++ b/server/api/controller/user.js @@ -4,7 +4,7 @@ const crypto = require('crypto') const jwt = require('jsonwebtoken') const Mastodon = require('mastodon-api') const { Op } = require('sequelize') - +const jsonwebtoken = require('jsonwebtoken') const User = require('../models/user') const config = require('../config') const mail = require('../mail') @@ -27,18 +27,24 @@ const userController = { } else { // if user is found and password is right // create a token - const payload = { email: user.email } - const token = jwt.sign(payload, config.secret) - res.json({ - success: true, - message: 'Enjoy your token!', - token, - user - }) + const accessToken = jsonwebtoken.sign({ user: + { + id: user.id, + email: user.email, + scope: [user.is_admin ? 'admin' : 'user'] + }}, + config.secret + ) + + res.json({token: accessToken}) } } }, + async logout(req, res) { + + }, + async setToken(req, res) { req.user.mastodon_auth = req.body await req.user.save() diff --git a/server/api/index.js b/server/api/index.js index ebf7fb8b..19f253c1 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -5,8 +5,10 @@ const eventController = require('./controller/event') const exportController = require('./controller/export') const userController = require('./controller/user') const settingsController = require('./controller/settings') +const config = require('./config') // const botController = require('./controller/bot') +const jwt = require('express-jwt')({secret: config.secret}) const storage = require('./storage')({ destination: 'uploads/' @@ -14,8 +16,12 @@ const storage = require('./storage')({ const upload = multer({ storage }) const api = express.Router() -// login -api.post('/login', userController.login) + +// AUTH +api.post('/auth/login', userController.login) +api.post('/auth/logout', userController.logout) +api.get('/auth/user', jwt, userController.current) + api.post('/user/recover', userController.forgotPassword) api.post('/user/check_recover_code', userController.checkRecoverCode) api.post('/user/recover_password', userController.updatePasswordWithRecoverCode) @@ -25,7 +31,7 @@ api // register .post(userController.register) // get current user - .get(isAuth, userController.current) + // .get(isAuth, userController.current) // update user (eg. confirm) .put(isAuth, isAdmin, userController.update) diff --git a/server/api/models/user.js b/server/api/models/user.js index 2fcfbac5..e6a13ff3 100644 --- a/server/api/models/user.js +++ b/server/api/models/user.js @@ -1,6 +1,6 @@ +const Sequelize = require('sequelize') const bcrypt = require('bcrypt') const db = require('../db') -const Sequelize = require('sequelize') const User = db.define('user', { email: { diff --git a/server/index.js b/server/index.js index edb79306..42ebd858 100644 --- a/server/index.js +++ b/server/index.js @@ -2,6 +2,7 @@ const express = require('express') const consola = require('consola') const morgan = require('morgan') const bodyParser = require('body-parser') +const cookieParser = require('cookie-parser') const { Nuxt, Builder } = require('nuxt') const app = express() const cors = require('cors') @@ -31,7 +32,7 @@ async function start() { // Give nuxt middleware to express app.use(cors(corsConfig)) app.use(morgan('dev')) - // app.set('views', path.join) + app.use(cookieParser()) app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.json()) app.use(nuxt.render) diff --git a/store/index.js b/store/index.js index f2172b0f..bd568e4a 100644 --- a/store/index.js +++ b/store/index.js @@ -1,19 +1,11 @@ import moment from 'dayjs' import { intersection } from 'lodash' -import api from '~/plugins/api' import Vue from 'vue' -Vue.config.errorHandler = function (err, vm, info) { - // handle error - // `info` is a Vue-specific error info, e.g. which lifecycle hook - // the error was found in. Only available in 2.2.0+ - console.error(err) - console.error(info) -} - export const state = () => ({ events: [], user: {}, + locale: 'it', logged: false, token: '', tags: [], @@ -109,18 +101,20 @@ export const actions = { // 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()) const events = await this.$axios.$get(`/event/${now.getMonth() - 1}/${now.getFullYear()}`) commit('setEvents', events) }, + async updateEvents({ commit }, page) { + const events = await this.$axios.$get(`/event/${page.month}/${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) { - console.log('ciao addEvent') const event = await this.$axios.$post('/user/event', formData) // .addEvent(formData) if (this.state.logged) { commit('addEvent', event) @@ -133,14 +127,6 @@ export const actions = { delEvent({ commit }, eventId) { commit('delEvent', eventId) }, - login({ commit }, user) { - this.$axios.setToken(user.token) - commit('login', user) - }, - logout({ commit }) { - this.$axios.setToken(false) - commit('logout') - }, // search addSearchTag({ commit }, tag) { commit('addSearchTag', tag) From ac5ef6e324d1abdd6c0dfdd3cdd438980076a004 Mon Sep 17 00:00:00 2001 From: lesion Date: Mon, 29 Apr 2019 00:27:29 +0200 Subject: [PATCH 06/14] mille storie commenti da mastodon, widget con custom widget test... --- .vscode/vscode-kanban.json | 72 +++++++++++++------ components/Calendar.vue | 47 +++++++++---- components/Event.vue | 6 +- components/Home.vue | 28 +++++--- components/List.vue | 34 +++++++++ components/Nav.vue | 7 +- layouts/iframe.vue | 3 + locales/it.json | 28 +++++++- nuxt.config.js | 6 +- package.json | 4 +- pages/{add.vue => add/_edit.vue} | 16 +++-- pages/admin.vue | 115 ++++++++++++++++++------------- pages/embed/list.vue | 22 ++++++ pages/event/_id.vue | 51 +++++++++----- pages/export.vue | 59 +++++++++------- pages/settings.vue | 2 +- plugins/element-ui.js | 8 ++- plugins/v-calendar.js | 4 +- plugins/vuex-persist.js | 15 ++-- server/api/auth.js | 51 +++++--------- server/api/config.js | 4 +- server/api/controller/bot.js | 72 +++++++++++-------- server/api/controller/event.js | 29 ++++---- server/api/controller/export.js | 2 + server/api/controller/user.js | 4 +- server/api/index.js | 35 ++++------ server/api/models/event.js | 15 ++-- server/cron.js | 65 +++++++++++++++++ server/index.js | 2 + store/index.js | 10 +-- widgets/examples/index.html | 17 +++++ widgets/list/index.html | 0 widgets/list/index.js | 12 ++++ widgets/list/style.css | 3 + 34 files changed, 573 insertions(+), 275 deletions(-) create mode 100644 components/List.vue create mode 100644 layouts/iframe.vue rename pages/{add.vue => add/_edit.vue} (94%) create mode 100644 pages/embed/list.vue create mode 100644 server/cron.js create mode 100644 widgets/examples/index.html create mode 100644 widgets/list/index.html create mode 100644 widgets/list/index.js create mode 100644 widgets/list/style.css diff --git a/.vscode/vscode-kanban.json b/.vscode/vscode-kanban.json index 47a6e887..9d48a542 100644 --- a/.vscode/vscode-kanban.json +++ b/.vscode/vscode-kanban.json @@ -1,6 +1,39 @@ { - "done": [], - "in-progress": [], + "done": [ + { + "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" + } + ], + "in-progress": [ + { + "assignedTo": { + "name": "lesion" + }, + "category": "feature", + "creation_time": "2019-04-23T19:46:46.332Z", + "id": "3", + "prio": 0, + "references": [], + "title": "export lista" + } + ], "testing": [ { "assignedTo": { @@ -15,18 +48,6 @@ } ], "todo": [ - { - "assignedTo": { - "name": "lesion" - }, - "category": "feature", - "creation_time": "2019-04-23T19:50:00.973Z", - "id": "7", - "prio": 1, - "references": [], - "title": "export page", - "type": "bug" - }, { "assignedTo": { "name": "lesion" @@ -99,21 +120,19 @@ "name": "lesion" }, "category": "feature", - "creation_time": "2019-04-23T19:46:46.332Z", - "id": "3", - "prio": 0, + "creation_time": "2019-04-23T19:44:56.705Z", + "id": "1", "references": [], - "title": "export lista" + "title": "popup sul calendario" }, { "assignedTo": { "name": "lesion" }, - "category": "feature", - "creation_time": "2019-04-23T19:44:56.705Z", - "id": "1", + "creation_time": "2019-04-27T19:44:33.769Z", + "id": "12", "references": [], - "title": "popup sul calendario" + "title": "rifare il calendario o solo il popup" }, { "assignedTo": { @@ -137,6 +156,15 @@ "references": [], "title": "documentare sorgenti", "type": "bug" + }, + { + "assignedTo": { + "name": "lesion" + }, + "creation_time": "2019-04-28T09:25:50.701Z", + "id": "13", + "references": [], + "title": "test altra visualizzazione" } ] } \ No newline at end of file diff --git a/components/Calendar.vue b/components/Calendar.vue index 7787d5f4..d3c06ff1 100644 --- a/components/Calendar.vue +++ b/components/Calendar.vue @@ -1,15 +1,24 @@ \ No newline at end of file diff --git a/components/Nav.vue b/components/Nav.vue index 67a682e2..d10cf8c7 100644 --- a/components/Nav.vue +++ b/components/Nav.vue @@ -10,12 +10,13 @@ span.d-md-none {{$t('common.add_event')}} b-nav-item(v-if='$auth.loggedIn' to='/settings' v-b-tooltip :title='$t("common.settings")') span.d-md-none {{$t('common.settings')}} - b-nav-item(v-if='$auth.hasScope(`admin`)' to='/admin' v-b-tooltip :title='$t("common.admin")') + b-nav-item(v-if='$auth.user.is_admin' to='/admin' v-b-tooltip :title='$t("common.admin")') span.d-md-none {{$t('common.admin')}} b-nav-item(to='/export' v-b-tooltip :title='$t("common.export")') span.d-md-none {{$t('common.export')}} - b-nav-item(v-if='auth.loggedIn' @click='logout' v-b-tooltip :title='$t("common.logout")') + b-nav-item(v-if='$auth.loggedIn' @click='logout' v-b-tooltip :title='$t("common.logout")') span.d-md-none {{$t('common.logout')}} + b-nav-item b-navbar-nav.ml-auto b-nav-item(to='/about') span {{$t('common.info')}} @@ -26,7 +27,7 @@ import {mapState, mapActions} from 'vuex' export default { name: 'Nav', computed: { - ...mapState(['filters', 'auth']), + ...mapState(['filters']), filters_tags: { set (value) { this.setSearchTags(value) diff --git a/layouts/iframe.vue b/layouts/iframe.vue new file mode 100644 index 00000000..858c52d1 --- /dev/null +++ b/layouts/iframe.vue @@ -0,0 +1,3 @@ + diff --git a/locales/it.json b/locales/it.json index efa952cb..6f37cec2 100644 --- a/locales/it.json +++ b/locales/it.json @@ -17,7 +17,25 @@ "next": "Avanti", "what": "Cosa", "address": "Indirizzo", - "media": "Media" + "media": "Media", + "today": "Oggi", + "users": "Utenti", + "places": "Posti", + "events": "Eventi", + "tags": "Etichette", + "deactivate": "Disattiva", + "activate": "Attiva", + "remove_admin": "Rimuovi Admin", + "name": "Nome", + "save": "Salva", + "confirm": "Conferma", + "tag": "Etichetta", + "color": "Colore", + "associate": "Associa", + "hide": "Nascondi", + "remove": "Elimina", + "edit": "Modifica", + "actions": "Azioni" }, "login": { @@ -41,6 +59,14 @@ "where_description" : "Dov'è il gancio? Se è un luogo fisico, scrivi il suo nome del per esteso (tipo 'Mezcal Squat'), se è una Piazza/Via metti quella (tipo 'Piazza Castello, Torino'). Se trovi già il luogo dell'evento premilo e l'indirizzo verrà autocompletato.", "address_description": "", "tag_description": "Puoi inserire un tag (es. concerto, corteo)" + }, + + "admin": { + "event_confirm_description": "Qui puoi confermare gli eventi inseriti da utenti non iscritti", + "tag_description": "Cambia il colore delle etichette", + "place_description": "Modifica l'indirizzo di un posto", + "mastodon_description": "Puoi associare un account mastodon a questa istanza di gancio. Tutti gli eventi verranno pubblicati li'", + "mastodon_instance": "Istanza mastodon" } } diff --git a/nuxt.config.js b/nuxt.config.js index 29c34492..30bdf1da 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -41,8 +41,8 @@ module.exports = { plugins: ['@/plugins/element-ui', '@/plugins/filters', '@/plugins/i18n', '@/plugins/bootstrap-vue', '@/plugins/vue-awesome', - '@/plugins/v-calendar', - { src: '@/plugins/vuex-persist', ssr: false }, + { src: '@/plugins/v-calendar', ssr: false }, + { src: '@/plugins/vuex-persist.js', ssr: false }, '@/plugins/magic-grid'], /* @@ -68,7 +68,7 @@ module.exports = { endpoints: { login: { url: '/auth/login', method: 'post', propertyName: 'token' }, logout: { url: '/auth/logout', method: 'post' }, - user: { url: '/auth/user', method: 'get', propertyName: 'user' } + user: { url: '/auth/user', method: 'get', propertyName: false } }, // tokenRequired: true, // tokenType: 'bearer', diff --git a/package.json b/package.json index ecb78137..73aa6086 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "sqlite3": "^4.0.6", "v-calendar": "^0.9.7", "vue-awesome": "^3.5.1", + "vue-custom-element": "^3.2.6", "vue-i18n": "^8.10.0", "vue-magic-grid": "^0.0.4", "vuex-persist": "^2.0.0" @@ -61,6 +62,7 @@ "eslint-plugin-vue": "^5.2.2", "nodemon": "^1.18.9", "prettier": "^1.16.4", - "pug-plain-loader": "^1.0.0" + "pug-plain-loader": "^1.0.0", + "webpack-cli": "^3.3.1" } } diff --git a/pages/add.vue b/pages/add/_edit.vue similarity index 94% rename from pages/add.vue rename to pages/add/_edit.vue index ae5c2ac1..f9a8ae44 100644 --- a/pages/add.vue +++ b/pages/add/_edit.vue @@ -5,7 +5,7 @@ el-tabs.mb-2(v-model='activeTab' v-loading='sending') //- NOT LOGGED EVENT - el-tab-pane(v-if='!logged') + 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')}} @@ -13,7 +13,14 @@ //- WHERE el-tab-pane span(slot='label') {{$t('common.where')}} - div {{$t('common.where')}} + div {{$t('common.where')}} + el-popover( + placement="top-start" + width="400" + trigger="hover") + v-icon(slot='reference' color='#ff9fc4' name='question-circle') + slot + p {{$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_name' :label='place' :value='place' :key='place.id') div {{$t("common.address")}} @@ -119,7 +126,6 @@ export default { places_name: state => state.places.map(p => p.name ), places: state => state.places, user: state => state.user, - logged: state => state.logged }), disableAddress () { console.log('dentro disable Address') @@ -128,7 +134,7 @@ export default { return ret }, couldProceed () { - const t = this.logged ? -1 : 0 + const t = this.$auth.loggedIn ? -1 : 0 switch(Number(this.activeTab)) { case 0+t: return true @@ -209,7 +215,7 @@ export default { this.updateMeta() this.sending = false this.$refs.modal.hide() - Message({ type: 'success', message: this.logged ? this.$t('event.added') : this.$t('event.added_anon')}) + Message({ type: 'success', message: this.$auth.loggedIn ? this.$t('event.added') : this.$t('event.added_anon')}) } catch (e) { this.sending = false console.error(e) diff --git a/pages/admin.vue b/pages/admin.vue index 0b83f1a1..edc22def 100644 --- a/pages/admin.vue +++ b/pages/admin.vue @@ -1,5 +1,5 @@ diff --git a/pages/event/_id.vue b/pages/event/_id.vue index 6873a2cf..935612cf 100644 --- a/pages/event/_id.vue +++ b/pages/event/_id.vue @@ -3,26 +3,26 @@ b-card(no-body, :img-src='imgPath' v-loading='loading') nuxt-link(to='/') el-button.close_button(circle icon='el-icon-close' type='success' - @click='$refs.eventDetail.hide()') + @click.prevent='$refs.eventDetail.hide()') b-card-header h3 {{event.title}} v-icon(name='clock') span {{event.start_datetime|datetime}} br v-icon(name='map-marker-alt') - //- span {{event.place.name}} - {{event.place.address}} + span {{event.place.name}} - {{event.place.address}} br b-card-body(v-if='event.description || event.tags') pre(v-html='event.description') br - el-tag.mr-1(:color='tag.color || "grey"' v-for='tag in event.tags' + el-tag.mr-1(:color='tag.color' v-for='tag in event.tags' size='mini' :key='tag.tag') {{tag.tag}} - .ml-auto(v-if='mine') + div(v-if='mine') hr - el-button(v-if='event.is_visible' plain type='warning' @click.prevents='toggle' icon='el-icon-view') {{$t('Unconfirm')}} - el-button(v-else plain type='success' @click.prevents='toggle' icon='el-icon-view') {{$t('Confirm')}} - el-button(plain type='danger' @click.prevent='remove' icon='el-icon-remove') {{$t('Remove')}} - el-button(plain type='primary' @click='$router.replace("/edit/"+event.id)') {{$t('Edit')}} + el-button(v-if='event.is_visible' plain type='warning' @click.prevents='toggle' icon='el-icon-view') {{$t('common.hide')}} + el-button(v-else plain type='success' @click.prevents='toggle' icon='el-icon-view') {{$t('common.confirm')}} + el-button(plain type='danger' @click.prevent='remove' icon='el-icon-remove') {{$t('common.remove')}} + el-button(plain type='primary' @click='$router.replace("/edit/"+event.id)') {{$t('common.edit')}} //- COMMENTS ... //- b-navbar(type="dark" variant="dark" toggleable='lg') @@ -34,10 +34,17 @@ //- span.mr-3 {{event.comments.length}} //- a(href='#', @click='remove') v-icon(color='orange' name='times') - //- b-card-footer(v-for='comment in event.comments') - strong {{comment.author}} - div(v-html='comment.text') + //- el-footer(v-for='comment in event.comments') + strong {{comment.author}} + div(v-html='comment.text') + //- el-timeline + //- el-timeline-item(v-for='comment in event.comments') + //- p(v-html='comment.text') + //- a.el-timeline-item__timestamp(href='') {{comment.createdAt}} + strong {{$t('common.comments')}} + div.text.item(v-for='comment in event.comments') + span(v-html='comment.text') + + + + + + + + + \ No newline at end of file diff --git a/widgets/list/index.html b/widgets/list/index.html new file mode 100644 index 00000000..e69de29b diff --git a/widgets/list/index.js b/widgets/list/index.js new file mode 100644 index 00000000..0b47f6d7 --- /dev/null +++ b/widgets/list/index.js @@ -0,0 +1,12 @@ +import Vue from 'vue' +import vueCustomElement from 'vue-custom-element' +import App from '../../components/List' +// import router from './router' +// import store from '../../store' + +Vue.use(vueCustomElement) + +// App.store = store +// App.router = router +Vue.customElement('gancio-widget', App) +export default App diff --git a/widgets/list/style.css b/widgets/list/style.css new file mode 100644 index 00000000..87eb01f5 --- /dev/null +++ b/widgets/list/style.css @@ -0,0 +1,3 @@ +#gancio-widget { + border: 1px solid black; +} \ No newline at end of file From e50e5434bc177f66456ac1a7d287f0d9b786d373 Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 30 Apr 2019 01:04:24 +0200 Subject: [PATCH 07/14] comment from mastodon to gancio --- .vscode/vscode-kanban.json | 49 ++++++++++++++---------- components/Nav.vue | 2 +- locales/it.json | 8 ++-- pages/event/_id.vue | 74 ++++++++++++++++++------------------ server/api/controller/bot.js | 7 +++- server/api/db.js | 2 +- server/api/models/event.js | 6 +-- 7 files changed, 81 insertions(+), 67 deletions(-) diff --git a/.vscode/vscode-kanban.json b/.vscode/vscode-kanban.json index 9d48a542..9e4d1a8f 100644 --- a/.vscode/vscode-kanban.json +++ b/.vscode/vscode-kanban.json @@ -32,6 +32,17 @@ "prio": 0, "references": [], "title": "export lista" + }, + { + "assignedTo": { + "name": "lesion" + }, + "category": "feature", + "creation_time": "2019-04-23T19:56:46.263Z", + "id": "11", + "prio": 1, + "references": [], + "title": "get comments / media from mastodon" } ], "testing": [ @@ -72,17 +83,6 @@ "title": "rivedere ux / messaggi utente", "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" @@ -115,6 +115,15 @@ "title": "risolvere le modali quando il js e' disabilitato", "type": "bug" }, + { + "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" @@ -134,6 +143,15 @@ "references": [], "title": "rifare il calendario o solo il popup" }, + { + "assignedTo": { + "name": "lesion" + }, + "creation_time": "2019-04-28T09:25:50.701Z", + "id": "13", + "references": [], + "title": "test altra visualizzazione" + }, { "assignedTo": { "name": "lesion" @@ -156,15 +174,6 @@ "references": [], "title": "documentare sorgenti", "type": "bug" - }, - { - "assignedTo": { - "name": "lesion" - }, - "creation_time": "2019-04-28T09:25:50.701Z", - "id": "13", - "references": [], - "title": "test altra visualizzazione" } ] } \ No newline at end of file diff --git a/components/Nav.vue b/components/Nav.vue index d10cf8c7..67f0f3f7 100644 --- a/components/Nav.vue +++ b/components/Nav.vue @@ -10,7 +10,7 @@ span.d-md-none {{$t('common.add_event')}} b-nav-item(v-if='$auth.loggedIn' to='/settings' v-b-tooltip :title='$t("common.settings")') span.d-md-none {{$t('common.settings')}} - b-nav-item(v-if='$auth.user.is_admin' to='/admin' v-b-tooltip :title='$t("common.admin")') + b-nav-item(v-if='$auth.user && $auth.user.is_admin' to='/admin' v-b-tooltip :title='$t("common.admin")') span.d-md-none {{$t('common.admin')}} b-nav-item(to='/export' v-b-tooltip :title='$t("common.export")') span.d-md-none {{$t('common.export')}} diff --git a/locales/it.json b/locales/it.json index 6f37cec2..00c1e34a 100644 --- a/locales/it.json +++ b/locales/it.json @@ -35,7 +35,9 @@ "hide": "Nascondi", "remove": "Elimina", "edit": "Modifica", - "actions": "Azioni" + "actions": "Azioni", + "resources": "Risorse", + "add": "Aggiungi" }, "login": { @@ -58,7 +60,8 @@ "anon_description": "", "where_description" : "Dov'è il gancio? Se è un luogo fisico, scrivi il suo nome del per esteso (tipo 'Mezcal Squat'), se è una Piazza/Via metti quella (tipo 'Piazza Castello, Torino'). Se trovi già il luogo dell'evento premilo e l'indirizzo verrà autocompletato.", "address_description": "", - "tag_description": "Puoi inserire un tag (es. concerto, corteo)" + "tag_description": "Puoi inserire un tag (es. concerto, corteo)", + "added": "Avento aggiunto" }, "admin": { @@ -69,4 +72,3 @@ "mastodon_instance": "Istanza mastodon" } } - diff --git a/pages/event/_id.vue b/pages/event/_id.vue index 935612cf..7fd32d5f 100644 --- a/pages/event/_id.vue +++ b/pages/event/_id.vue @@ -14,7 +14,6 @@ br b-card-body(v-if='event.description || event.tags') pre(v-html='event.description') - br el-tag.mr-1(:color='tag.color' v-for='tag in event.tags' size='mini' :key='tag.tag') {{tag.tag}} div(v-if='mine') @@ -24,27 +23,17 @@ el-button(plain type='danger' @click.prevent='remove' icon='el-icon-remove') {{$t('common.remove')}} el-button(plain type='primary' @click='$router.replace("/edit/"+event.id)') {{$t('common.edit')}} - //- COMMENTS ... - //- b-navbar(type="dark" variant="dark" toggleable='lg') - //- template(slot='footer') - //- b-navbar-nav - //- b-button(variant='success') {{$t('Share')}} - //- b-nav-item( {{$t('')}}) - //- b-card-footer.text-right - //- span.mr-3 {{event.comments.length}} - //- a(href='#', @click='remove') - v-icon(color='orange' name='times') - //- el-footer(v-for='comment in event.comments') - strong {{comment.author}} - div(v-html='comment.text') - - //- el-timeline - //- el-timeline-item(v-for='comment in event.comments') - //- p(v-html='comment.text') - //- a.el-timeline-item__timestamp(href='') {{comment.createdAt}} - strong {{$t('common.comments')}} - div.text.item(v-for='comment in event.comments') - span(v-html='comment.text') + b-card-body(v-if='event.activitypub_id') + strong {{$t('common.resources')}} - + a(:href='`https://mastodon.cisti.org/web/statuses/${event.activitypub_id}`') {{$t('common.add')}} + b-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') + //- span {{comment}} diff --git a/components/Event.vue b/components/Event.vue index 3a375e10..dd2fa9fc 100644 --- a/components/Event.vue +++ b/components/Event.vue @@ -1,65 +1,139 @@ - diff --git a/components/Home.vue b/components/Home.vue index edac84d1..db75cbab 100644 --- a/components/Home.vue +++ b/components/Home.vue @@ -1,108 +1,40 @@ - diff --git a/components/List.vue b/components/List.vue index a9dea5fc..f412e963 100644 --- a/components/List.vue +++ b/components/List.vue @@ -1,34 +1,83 @@ \ No newline at end of file + + diff --git a/components/Nav.vue b/components/Nav.vue index 67f0f3f7..013fdb2f 100644 --- a/components/Nav.vue +++ b/components/Nav.vue @@ -1,51 +1,59 @@ + diff --git a/components/Search.vue b/components/Search.vue index fbc3ed2d..16280382 100644 --- a/components/Search.vue +++ b/components/Search.vue @@ -1,23 +1,64 @@ + diff --git a/layouts/default.vue b/layouts/default.vue index fe2d459b..1a2ea589 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -1,71 +1,6 @@ - + diff --git a/pages/add/_edit.vue b/pages/add/_edit.vue index f9a8ae44..37047935 100644 --- a/pages/add/_edit.vue +++ b/pages/add/_edit.vue @@ -1,8 +1,7 @@ \ No newline at end of file diff --git a/pages/embed/list.vue b/pages/embed/list.vue index 8816a8e1..7ca16b0c 100644 --- a/pages/embed/list.vue +++ b/pages/embed/list.vue @@ -1,22 +1,26 @@ diff --git a/pages/event/_id.vue b/pages/event/_id.vue index 7fd32d5f..34483559 100644 --- a/pages/event/_id.vue +++ b/pages/event/_id.vue @@ -1,54 +1,81 @@ - diff --git a/pages/export.vue b/pages/export.vue index f722e2de..76c1907c 100644 --- a/pages/export.vue +++ b/pages/export.vue @@ -1,13 +1,12 @@ + diff --git a/pages/login.vue b/pages/login.vue index eac775f2..518a4982 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -1,15 +1,21 @@ @@ -17,18 +23,22 @@ const Cookie = process.client ? require('js-cookie') : undefined import { mapActions } from 'vuex' import { Message } from 'element-ui' -// import api from '@/plugins/api' +import get from 'lodash/get' export default { name: 'Login', data () { return { + open: true, password: '', email: '', loading: false } }, methods: { + close () { + this.$router.replace('/') + }, ...mapActions(['login']), async forgot () { if (!this.email) { @@ -37,7 +47,7 @@ export default { return } this.loading = true - // await api.forgotPassword(this.email) + await this.$axios.$post('/user/recover', { email: this.email }) this.loading = false Message({ message: this.$t('login.check_email'), type: 'success' }) }, @@ -49,12 +59,12 @@ export default { this.loading = false Message({ message: this.$t('login.ok'), type: 'success' }) } catch (e) { - Message({ message: this.$t('login.error') + e, type: 'error' }) + e = get(e, 'response.data.message', e) + Message({ message: this.$t('login.error') + this.$t(e), type: 'error' }) this.loading = false return } this.email = this.password = '' - this.$refs.modal.hide() } } } diff --git a/pages/register.vue b/pages/register.vue index c3b28087..b37d2917 100644 --- a/pages/register.vue +++ b/pages/register.vue @@ -1,9 +1,8 @@ @@ -36,5 +40,19 @@ section { width: 100%; max-width: 1500px; margin: 0 auto; + + .top { + position: fixed; + bottom: 10px; + right: 10px; + z-index: 1; + opacity: 0.7; + font-size: 16px; + } + + .totop { + position: absolute; + top: 0px; + } } diff --git a/components/Search.vue b/components/Search.vue index 16280382..5080cbd4 100644 --- a/components/Search.vue +++ b/components/Search.vue @@ -23,7 +23,6 @@ - diff --git a/layouts/iframe.vue b/layouts/iframe.vue index 858c52d1..2cc8469e 100644 --- a/layouts/iframe.vue +++ b/layouts/iframe.vue @@ -1,3 +1,3 @@ - @@ -153,7 +152,7 @@ export default { computed: { ...mapState({ tags: state => state.tags.map(t => t.tag ), - places_name: state => state.places.map(p => p.name ), + places_name: state => state.places.map(p => p.name ).sort((a, b) => b.weigth-a.weigth), places: state => state.places, user: state => state.user, events: state => state.events diff --git a/pages/admin.vue b/pages/admin.vue index fd8997b9..b15bbb01 100644 --- a/pages/admin.vue +++ b/pages/admin.vue @@ -27,7 +27,7 @@ template(slot='label') v-icon(name='map-marker-alt') span.ml-1 {{$t('common.places')}} - p {{$t('admin.place_description')}} + 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') @@ -201,7 +201,7 @@ export default { await this.$axios.$get(`/event/confirm/${id}`) this.loading = false Message({ - message: this.$t('common.event_confirmed'), + message: this.$t('event.confirmed'), type: 'success' }) this.events = this.events.filter(e => e.id !== id) diff --git a/pages/embed/list.vue b/pages/embed/list.vue index 7ca16b0c..5d224613 100644 --- a/pages/embed/list.vue +++ b/pages/embed/list.vue @@ -12,15 +12,18 @@ export default { components: { List }, async asyncData ({ $axios, req, res }) { const title = req.query.title || SHARED_CONF.title - const show_tags = req.query.showtags const tags = req.query.tags const places = req.query.places const now = new Date() - // TODO: filter future events based on tags/places/userid - const events = await $axios.$get(`/event/${now.getMonth()}/${now.getFullYear()}`) + let params = [] + if (places) params.push(`places=${places}`) + if (tags) params.push(`tags=${tags}`) - return { show_tags, events, title } + params = params.length ? `?${params.join('&')}` : '' + const events = await $axios.$get(`/export/json${params}`) + + return { events, title } }, } diff --git a/pages/event/_id.vue b/pages/event/_id.vue index 34483559..21e4916b 100644 --- a/pages/event/_id.vue +++ b/pages/event/_id.vue @@ -13,9 +13,9 @@ h5.text-center {{event.title}} div.nextprev nuxt-link(v-if='prev' :to='`/event/${prev.id}`') - el-button(icon='el-icon-arrow-left' round size='small' type='success' plain) + 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 size='small' plain type='success') + el-button(icon='el-icon-arrow-right' round type='success') //- image img(:src='imgPath' v-if='event.image_path') diff --git a/pages/export.vue b/pages/export.vue index 76c1907c..bfeab302 100644 --- a/pages/export.vue +++ b/pages/export.vue @@ -1,11 +1,11 @@