diff --git a/.dockerignore b/.dockerignore index 3285e1ae..bc2de8a4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,5 +5,6 @@ node_modules/ build/ npm-debug.log Dockerfile -.env +docker-compose*.yml .env-secrets +.env diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..0a3085cb --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# WEBAPP +WEBAPP_HOST = localhost +WEBAPP_PORT = 3000 +WEBAPP_BASE_URL = http://localhost:3000 + +# API +API_HOST = localhost +API_PORT = 3030 + +# METAAPI +EMBED_API_URL = http://localhost:3050 + +# 3rd party integrations +SENTRY_DNS_PUBLIC = +MAPBOX_TOKEN = pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ + +# MAINTENANCE +MAINTENANCE = diff --git a/.env.tmp b/.env.tmp deleted file mode 100644 index 391cc5e7..00000000 --- a/.env.tmp +++ /dev/null @@ -1,35 +0,0 @@ -# DO NOT INPUT SECRETS OR PRIVAT TOKENS! -# for secrets please use server/.env-secrets -# -# this file here is read on server and client - -#################################### -# at build time -#################################### -BUILD_DATE = ${BUILD_DATE} -BUILD_ID = ${TRAVIS_BUILD_NUMBER} -BUILD_COMMIT = ${TRAVIS_PULL_REQUEST_SHA} - -#################################### -# at deploy time -#################################### -DEPLOY_DATE = ${DEPLOY_DATE} - -# WEBAPP -WEBAPP_HOST = ${WEBAPP_HOST} -WEBAPP_PORT = ${WEBAPP_PORT} -WEBAPP_BASE_URL = ${WEBAPP_BASE_URL} - -# API -API_HOST = ${API_HOST} -API_PORT = ${API_PORT} - -# METAAPI -EMBED_API_URL = ${EMBED_API_URL} - -# 3rd party integrations -SENTRY_DNS_PUBLIC = ${SENTRY_DNS_PUBLIC} -MAPBOX_TOKEN = ${MAPBOX_TOKEN} - -# MAINTENANCE -MAINTENANCE = ${MAINTENANCE} diff --git a/.gitignore b/.gitignore index 43ab0ac6..86bb3684 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ npm-debug.log /nbproject/private/ /nbproject/ -# .env and .env-secrets .env .env-secrets diff --git a/.travis.yml b/.travis.yml index 5ac3e772..ff558afe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,10 +16,9 @@ jobs: - stage: Test and Build script: - yarn install --frozen-lockfile --non-interactive - - yarn test:env - - yarn test + - yarn run ci - script: - - docker build -t humanconnection/frontend-nuxt . + - docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT -t humanconnection/frontend-nuxt . after_success: - if [ $TRAVIS_BRANCH == "master" ] && [ $TRAVIS_EVENT_TYPE == "push" ]; then diff --git a/Dockerfile b/Dockerfile index 9b0e039d..ff14ac13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,46 +1,28 @@ -FROM node:8.9-alpine +FROM node:alpine LABEL Description="This image is used to start the hc-frontend-nuxt" Vendor="Human-Connection gGmbH" Version="1.0" Maintainer="Human-Connection gGmbH (developer@human-connection.org)" -# update unix packages -RUN apk update && apk upgrade -RUN apk add git -RUN rm -rf /var/cache/apk/* - -# install global dependencies -RUN yarn global add pm2 envsub - # expose the app port EXPOSE 3000 -# set environment variables -ENV HOST=0.0.0.0 -ENV WEBAPP_HOST=0.0.0.0 - -ENTRYPOINT ["./entrypoint.sh"] - -# create working directory -RUN mkdir -p /var/www/ -WORKDIR /var/www/ - -# install app dependencies -COPY package.json /var/www/ -COPY yarn.lock /var/www/ -RUN yarn install --frozen-lockfile --non-interactive +# optional git commit hash +ARG BUILD_COMMIT +ENV BUILD_COMMIT=$BUILD_COMMIT +ENV NODE_ENV=production -# copy the code to the docker image -COPY . /var/www/ +RUN mkdir -p /WebApp/ +WORKDIR /WebApp/ +# --no-cache: download package index on-the-fly, no need to cleanup afterwards +# --virtual: bundle packages, remove whole bundle at once, when done +RUN apk --no-cache --virtual build-dependencies add git python make g++ -# set execution rights on scripts and run the build script -RUN chmod +x entrypoint.sh -RUN chmod +x scripts/on-build.sh -RUN chmod +x scripts/on-start.sh -RUN sh scripts/on-build.sh +RUN yarn global add pm2 -# buld application -# ENV NODE_ENV=production #we seam to have issues with the production flag on install && build -RUN yarn build +COPY package.json /WebApp/ +COPY yarn.lock /WebApp/ +RUN yarn install --production=false --frozen-lockfile --non-interactive -ENV NODE_ENV=production +RUN apk del build-dependencies -# only keep production dependencies -# RUN yarn install --frozen-lockfile --non-interactive +COPY . /WebApp/ +RUN ["yarn", "run", "build"] +CMD ["pm2", "start", "node", "build/main.js", "-n", "frontend", "-i", "2", "--attach"] diff --git a/README.md b/README.md index 32217340..af05c333 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This is the nuxt + express version of our WebApp as nuxt.js seams to be more stable and we have better options for keeping it updated. -## Build Setup +## Local installation > we recommend to install the project locally for the best development ease and performance @@ -34,6 +34,12 @@ $ yarn dev $ yarn start ``` +Create your individual set of environment variables: +```sh +$ cp .env.example .env +# now open .env and change it according to your setup +``` + For detailed explanation on how things work, checkout the [Nuxt.js docs](https://github.com/nuxt/nuxt.js). ## Env Vars diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..db7d549d --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,68 @@ +version: '3' + +services: + webapp: + networks: + hc-network: + ipv4_address: 172.25.0.20 + environment: + - NODE_ENV=development + - MAINTENANCE=${MAINTENANCE} + - WEBAPP_HOST=172.25.0.20 + - WEBAPP_BASE_URL=http://172.25.0.20:3000 + - API_HOST=172.25.0.11 + - WEBAPP_PORT=3000 + - API_PORT=3030 + - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ + - SENTRY_DNS_PUBLIC=${SENTRY_DNS_PUBLIC} + - EMBED_API_URL=${EMBED_API_URL} + # these secrets should only be accessible on the server: + - SENTRY_DNS_PRIVATE=${SENTRY_DNS_PRIVATE} + - EMBED_API_TOKEN=${EMBED_API_TOKEN} + depends_on: + - api + mongo: + image: mongo + networks: + - hc-network + command: "--smallfiles --logpath=/dev/null" + api: + image: humanconnection/api-feathers + depends_on: + - maildev + - thumbor + - mongo + environment: + - NODE_ENV=staging + ports: + - "3030:3030" + stdin_open: true + tty: true + networks: + hc-network: + ipv4_address: 172.25.0.11 + thumbor: + networks: + - hc-network + maildev: + image: djfarrelly/maildev + networks: + - hc-network + ports: + - "1080:80" + - "1025:25" + thumbor: + image: apsl/thumbor + networks: + - hc-network + ports: + - "8000:8000" + +networks: + hc-network: + driver: bridge + ipam: + driver: default + config: + - + subnet: 172.25.0.0/16 diff --git a/docker-compose.yml b/docker-compose.yml index 02600987..196c2c97 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,13 @@ -version: '2' +version: '3' services: - frontend: - build: . - environment: - WEBAPP_HOST: localhost - WEBAPP_PORT: 3000 - WEBAPP_BASE_URL: http://localhost:3000 - API_HOST: http://localhost - API_PORT: 3030 - MAPBOX_TOKEN: + webapp: + image: humanconnection/frontend-nuxt + build: + context: . + args: + BUILD_COMMIT: ${BUILD_COMMIT} stdin_open: true tty: true ports: - - 3000:3000/tcp + - 3000:3000/tcp diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index d073171f..00000000 --- a/entrypoint.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env ash - -# this script is triggered always when the image is started -# it writes the environment variables to the config files -# this is needed as nuxtjs replaces all mentions of process.env -# with static vars at build time but we need them fresh at deploy time - -# setting deploy time environment vars and writing them to .env -./scripts/on-start.sh - -# starting the application with the correct settings -yarn start -p $WEBAPP_PORT -H $WEBAPP_HOST diff --git a/helpers/createApiClient.js b/helpers/createApiClient.js new file mode 100644 index 00000000..87c50ead --- /dev/null +++ b/helpers/createApiClient.js @@ -0,0 +1,65 @@ +import feathers from '@feathersjs/feathers' +import socketio from '@feathersjs/socketio-client' +import io from 'socket.io-client' +import authentication from '@feathersjs/authentication-client' +import urlHelper from '~/helpers/urls' +import Cookie from 'cookie-universal' + +const authKey = 'feathers-jwt' +const endpoint = urlHelper.buildEndpointURL(process.env.API_HOST, { port: process.env.API_PORT }) +let socket +if (process.env.ENV === 'production') { + socket = socketio(io(endpoint), { timeout: 20000 }) + if (process.server) { + setTimeout(() => { + // close server connection as content was delivered already after 30 seconds at latest + try { + socket.close() + } catch (err) { + console.log(err) + } + }, 30000) + } +} else { + socket = socketio(io(endpoint)) +} + +let createApiClient = ({req, res}) => { + const cookies = Cookie(req, res) + const storageMapping = { + getItem: (key) => { + const res = cookies.get(key) + // console.log(`## STORAGE: getItem(${key})`, res) + return res + }, + setItem: (key, value, options) => { + const res = cookies.set(key, value, options) + // console.log(`## STORAGE: setItem(${key}, ${value}, ${options})`, res) + return res + }, + removeItem: (key) => { + const res = cookies.remove(key) + // console.log(`## STORAGE: removeItem(${key})`, res) + return res + }, + clear: () => { + const res = cookies.removeAll() + if (process.env.NODE_ENV === 'development') { + console.log(`## STORAGE: clear()`, res) + } + return res + } + } + + let api = feathers() + .configure(socket) + .configure(authentication({ + storage: storageMapping, + storageKey: authKey, + cookie: authKey + })) + + return api +} + +export default createApiClient diff --git a/layouts/blank.vue b/layouts/blank.vue index 8b112a0d..36f24303 100644 --- a/layouts/blank.vue +++ b/layouts/blank.vue @@ -38,7 +38,7 @@ }, title: 'loading...' } - if (process.env.NODE_ENV === 'development') { + if (this.$env.NODE_ENV === 'development') { head.script = [ { src: 'https://cdn.jsdelivr.net/npm/tota11y@0.1.6/build/tota11y.min.js' } ] diff --git a/layouts/default.vue b/layouts/default.vue index ae9822a7..5b39ab31 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -31,7 +31,7 @@ }, title: 'loading...' } - if (process.env.NODE_ENV === 'development') { + if (this.$env.NODE_ENV === 'development') { head.script = [ { src: 'https://cdn.jsdelivr.net/npm/tota11y@0.1.6/build/tota11y.min.js' } ] diff --git a/layouts/error.vue b/layouts/error.vue index bc03d25b..d7dcf19c 100644 --- a/layouts/error.vue +++ b/layouts/error.vue @@ -38,7 +38,7 @@ } }, mounted () { - if (process.env.NODE_ENV === 'development') { + if (this.$env.NODE_ENV === 'development') { console.error(this.error.message) } diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 00000000..fe972f56 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,8 @@ +// If it's a private page and there's no payload, redirect. +export default function (context) { + const { store, redirect, route } = context + const { auth } = store.state + if (!auth.publicPages.includes(route.name) && !auth.payload) { + return redirect('/auth/login') + } +} diff --git a/middleware/authenticated.js b/middleware/authenticated.js deleted file mode 100644 index b74c0363..00000000 --- a/middleware/authenticated.js +++ /dev/null @@ -1,26 +0,0 @@ -import { isEmpty } from 'lodash' - -export default async ({ store, route, redirect }) => { - let publicPages = process.env.publicPages - publicPages.push('auth-logout') - // only affect non public pages - if (publicPages.indexOf(route.name) >= 0) { - return true - } - // await store.dispatch('auth/refreshJWT', 'authenticated middleware') - const isAuthenticated = await store.dispatch('auth/checkAuth') - if (isAuthenticated === true) { - return true - } - - // try to logout user - // await store.dispatch('auth/logout', null, { root: true }) - - // set the redirect path for after the login - let params = {} - if (!isEmpty(route.path) && route.path !== '/') { - params.path = route.path - } - - return redirect('/auth/login', params) -} diff --git a/nuxt.config.js b/nuxt.config.js index 6e88623d..c26205b2 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -1,18 +1,20 @@ -require('dotenv').config() const path = require('path') +const envWhitelist = [ + 'NODE_ENV', + 'WEBAPP_HOST', + 'WEBAPP_PORT', + 'WEBAPP_BASE_URL', + 'API_HOST', + 'API_PORT', + 'EMBED_API_URL', + 'SENTRY_DNS_PUBLIC', + 'MAPBOX_TOKEN', + 'MAINTENANCE' +] module.exports = { env: { // pages which do not require a login - publicPages: [ - 'auth-login', - 'auth-register', - 'auth-signup', - 'auth-reset', - 'auth-reset-token', - 'pages-slug', - 'test' - ], // pages to keep alive keepAlivePages: [ 'index' @@ -93,7 +95,6 @@ module.exports = { } }, plugins: [ - {src: '~/plugins/env.js'}, {src: '~/plugins/debug.js', ssr: false}, {src: '~/plugins/raven-client.js', ssr: false}, {src: '~/plugins/api.js'}, @@ -112,15 +113,15 @@ module.exports = { {src: '~/plugins/open-page-in-modal.js', ssr: false} ], modules: [ - 'cookie-universal-nuxt', - '@nuxtjs/dotenv' + ['@nuxtjs/dotenv', { only: envWhitelist }], + ['nuxt-env', { keys: envWhitelist }], + 'cookie-universal-nuxt' // '@nuxtjs/pwa' ], router: { middleware: [ 'maintenance', - 'check-auth', - 'authenticated' + 'auth' ], linkActiveClass: 'active-link' }, diff --git a/package.json b/package.json index c25625b5..74c87be7 100644 --- a/package.json +++ b/package.json @@ -27,22 +27,17 @@ "yarn": ">= 1.3.0" }, "scripts": { - "dev": "yarn dev:env && backpack dev", - "dev:local": "sh scripts/run-local.sh", - "dev:debug": "yarn dev:env && backpack dev --inspect-brk=9229", - "dev:env": "sh scripts/on-dev.sh", - "build": "yarn build:env && nuxt build && backpack build", - "build:env": "sh scripts/on-build.sh", - "start": "yarn start:env && node build/main.js", - "start:pm2": "yarn start:env && pm2 start node build/main.js -n frontend -i 2 --attach", - "start:env": "sh scripts/on-start.sh", - "refresh": "rm -rf node_modules && yarn install && npm run dev", + "ci": "yarn run eslint && yarn run test", + "clean": "rm -rf build/*", + "dev": "backpack dev", + "dev:debug": "backpack dev --inspect=0.0.0.0:9229", + "build": "nuxt build && backpack build", + "start": "node build/main.js", "precommit": "yarn eslint", "eslint": "eslint --ext .js,.vue .", "styleguide": "vue-styleguidist server", "styleguide:build": "vue-styleguidist build", "test": "ava", - "test:env": "sh scripts/on-dev.sh", "cypress:open": "cypress open", "cypress:run": "cypress run", "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov" @@ -52,29 +47,32 @@ "@feathersjs/client": "^3.5.3", "@feathersjs/feathers": "^3.1.7", "@feathersjs/socketio": "^3.2.2", - "@nuxtjs/dotenv": "~1.1.0", + "@nuxtjs/dotenv": "^1.1.1", "@nuxtjs/pwa": "~2.0.5", "axios": "^0.18.0", "babel-eslint": "~8.2.5", + "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-vue-app": "~2.0.0", "body-parser": "~1.18.3", "body-scroll-lock": "^2.4.6", "bricks.js": "~1.8.0", + "browser-env": "^3.2.5", "buefy": "~0.6.5", "bulma": "~0.7.1", "bulma-steps-component": "^0.5.3", "cheerio": "^1.0.0-rc.2", "cookie-parser": "~1.4.3", + "cookie-universal": "^2.0.5", "cookie-universal-nuxt": "~2.0.3", "cross-env": "~5.2.0", "css-loader": "~0.28.10", "csv-parse": "~2.4.0", "emitter-js": "~2.0.1", - "envsub": "~3.1.0", "express": "~4.16.2", "express-healthcheck": "~0.1.0", "express-locale": "~1.0.5", "express-session": "~1.15.6", + "feathers-vuex": "^1.4.8", "font-awesome": "~4.7.0", "helmet": "^3.12.1", "i18n-iso-countries": "^3.6.2", @@ -86,6 +84,7 @@ "mapbox-gl": "~0.45.0", "moment": "^2.22.2", "nuxt": "~1.4.0", + "nuxt-env": "^0.0.3", "parchment": "^1.1.4", "quill": "^1.3.6", "quill-delta": "^3.6.2", @@ -113,6 +112,7 @@ "vue-server-renderer": "~2.5.16", "vue-styleguidist": "~1.7.7", "vue-template-compiler": "~2.5.16", + "vue-test-utils": "^1.0.0-beta.11", "vue-text-glitch": "^1.0.1", "vuelidate": "~0.6.1", "vuex-i18n": "~1.10.5", @@ -124,7 +124,6 @@ "@vue/test-utils": "^1.0.0-beta.20", "ava": "~0.25.0", "backpack-core": "~0.7.0", - "browser-env": "~3.2.4", "codecov": "~3.0.4", "eslint": "~4.19.1", "eslint-config-standard": "~11.0.0-beta.0", @@ -136,7 +135,6 @@ "eslint-plugin-standard": "~3.1.0", "eslint-plugin-vue": "^4.5.0", "istanbul": "~0.4.5", - "jsdom": "~11.10.0", "less": "~2.7.3", "less-loader": "^4.1.0", "node-sass": "~4.9.0", diff --git a/pages/auth/name.vue b/pages/auth/name.vue index 8169f211..3e7bd960 100644 --- a/pages/auth/name.vue +++ b/pages/auth/name.vue @@ -65,7 +65,6 @@ import RandomAvataaar from '~/components/Avatar/RandomAvataaar' export default { - middleware: 'authenticated', layout: 'blank', components: { RandomAvataaar diff --git a/pages/auth/welcome.vue b/pages/auth/welcome.vue index 9dfdcbf6..f9e06a8a 100644 --- a/pages/auth/welcome.vue +++ b/pages/auth/welcome.vue @@ -31,7 +31,6 @@ import {mapGetters} from 'vuex' export default { - middleware: 'authenticated', layout: 'blank', computed: { ...mapGetters({ diff --git a/pages/contributions/write.vue b/pages/contributions/write.vue index f241871f..9291ff06 100644 --- a/pages/contributions/write.vue +++ b/pages/contributions/write.vue @@ -15,7 +15,7 @@ import ContributionsForm from '~/components/Contributions/ContributionsForm.vue' export default { - middleware: ['authenticated', 'verified'], + middleware: ['verified'], mixins: [animatable], components: { ContributionsForm diff --git a/pages/organizations/_slug.vue b/pages/organizations/_slug.vue index 8ab31967..069b9d06 100644 --- a/pages/organizations/_slug.vue +++ b/pages/organizations/_slug.vue @@ -183,7 +183,6 @@ uploadingLogo: false } }, - middleware: ['authenticated'], async asyncData ({app, params, store, error}) { let organization if (!isEmpty(params) && !isEmpty(params.slug) && params.slug !== undefined) { diff --git a/pages/organizations/create.vue b/pages/organizations/create.vue index add88a9d..a9a65329 100644 --- a/pages/organizations/create.vue +++ b/pages/organizations/create.vue @@ -79,7 +79,6 @@ import OrgaFormStep2 from "~/components/Organizations/steps/OrgaFormStep2.vue"; import OrgaFormStep3 from "~/components/Organizations/steps/OrgaFormStep3.vue"; export default { - middleware: "authenticated", layout: "blank", mixins: [animatable], components: { diff --git a/pages/profile/_slug.vue b/pages/profile/_slug.vue index 8ac3e65c..302549f5 100644 --- a/pages/profile/_slug.vue +++ b/pages/profile/_slug.vue @@ -202,7 +202,6 @@ isLoadingCanDos: false } }, - middleware: ['authenticated'], async asyncData ({ app, params, store, error }) { let user let isOwner = false diff --git a/plugins/api.js b/plugins/api.js index 29633145..a785344d 100644 --- a/plugins/api.js +++ b/plugins/api.js @@ -1,66 +1,21 @@ -import feathers from '@feathersjs/feathers' -import socketio from '@feathersjs/socketio-client' -import io from 'socket.io-client' -import authentication from '@feathersjs/authentication-client' -import urlHelper from '~/helpers/urls' +import feathersVuex from 'feathers-vuex' import Vue from 'vue' +import Vuex from 'vuex' +import createApiClient from '../helpers/createApiClient' -export default ({app, store, redirect, router}) => { - const authKey = 'feathers-jwt' - const endpoint = urlHelper.buildEndpointURL(app.$env.API_HOST, { port: app.$env.API_PORT }) - const storage = { - getItem: (key) => { - const res = app.$cookies.get(key) - // console.log(`## STORAGE: getItem(${key})`, res) - return res - }, - setItem: (key, value, options) => { - const res = app.$cookies.set(key, value, options) - // console.log(`## STORAGE: setItem(${key}, ${value}, ${options})`, res) - return res - }, - removeItem: (key) => { - const res = app.$cookies.remove(key) - // console.log(`## STORAGE: removeItem(${key})`, res) - return res - }, - clear: () => { - const res = app.$cookies.removeAll() - if (process.env.NODE_ENV === 'development') { - console.log(`## STORAGE: clear()`, res) - } - return res - } - } +export default ({app, store, redirect, router, req, res}) => { + const api = createApiClient({req, res}) + const { FeathersVuex } = feathersVuex(api, { idField: '_id' }) - const socket = io(endpoint) + Vue.use(FeathersVuex) + Vue.use(Vuex) - if (process.server) { - setTimeout(() => { - // close server connection as content was delivered already after 30 seconds at latest - try { - socket.close() - } catch (err) { - app.error(err) - } - }, 30000) - } - - let api = feathers() - .configure(socketio(socket, { - timeout: 20000 - })) - .configure(authentication({ - storage: storage, - storageKey: authKey, - cookie: authKey - })) api.hooks({ before: { all: [ async (hook) => { // hook.accessToken = await api.passport.getJWT() - if (process.env.NODE_ENV === 'development') { + if (app.$env.NODE_ENV === 'development') { console.log('# API:', `${hook.method} ${hook.path}`) console.info('data', hook.data) // console.log('# ' + hook.accessToken) @@ -70,10 +25,10 @@ export default ({app, store, redirect, router}) => { ] }, async error (ctx) { - if (process.env.NODE_ENV === 'development') { + if (app.$env.NODE_ENV === 'development') { console.log('####################') console.error(ctx.error) - console.info('JWT TOKEN: ', app.$cookies.get(authKey)) + // console.info('JWT TOKEN: ', app.$cookies.get(authKey)) console.info('path', ctx.path) console.info('service', ctx.service) console.info('method', ctx.method) @@ -103,7 +58,6 @@ export default ({app, store, redirect, router}) => { // fix issues where we could not log in // when at development await api.passport.logout() - storage.removeItem(authKey) } let user = null const response = await api.authenticate(options) @@ -120,11 +74,6 @@ export default ({app, store, redirect, router}) => { api.channel('authenticated').join(connection) }) - /** - * @deprecated - */ - api.authKey = authKey - // make the api accessible inside vue components Vue.use({ install (Vue) { diff --git a/plugins/env.js b/plugins/env.js deleted file mode 100644 index 62005f95..00000000 --- a/plugins/env.js +++ /dev/null @@ -1,14 +0,0 @@ -import Vue from 'vue' - -export default async (context) => { - await context.store.dispatch('env/init', context) - context.app.$env = context.store.getters['env/all'] - - Vue.use({ - install (Vue, store) { - Vue.prototype.$env = context.app.$env - } - }, context.store) - - return context.app.$env -} diff --git a/plugins/i18n.js b/plugins/i18n.js index dc006ab2..b581e0ea 100644 --- a/plugins/i18n.js +++ b/plugins/i18n.js @@ -5,7 +5,7 @@ import { debounce, isEmpty } from 'lodash' import vuexI18n from 'vuex-i18n/dist/vuex-i18n.umd.js' export default ({ app, req, cookie, store }) => { - const doDebug = process.env.NODE_ENV !== 'production' + const doDebug = app.$env.NODE_ENV !== 'production' const key = 'locale' const changeHandler = debounce((mutation, store) => { diff --git a/plugins/raven-server.js b/plugins/raven-server.js index 309cf0b4..f8d11ed2 100644 --- a/plugins/raven-server.js +++ b/plugins/raven-server.js @@ -1,15 +1,10 @@ import Raven from 'raven' -import dotenv from 'dotenv' -import {readFileSync} from 'fs' -import {resolve} from 'path' export default async function (app) { - const secrets = dotenv.parse(await readFileSync(resolve('./server/.env-secrets'))) - - if (!process.client && secrets.SENTRY_DNS_PRIVATE) { + if (!process.client && process.env.SENTRY_DNS_PRIVATE) { // LOGGING IS ENABLED console.log('SENTRY LOGGING IS ENABLED') - Raven.config(secrets.SENTRY_DNS_PRIVATE, { + Raven.config(process.env.SENTRY_DNS_PRIVATE, { release: app.$env.BUILD_COMMIT, environment: app.$env.NODE_ENV, tags: { diff --git a/scripts/on-build.sh b/scripts/on-build.sh deleted file mode 100644 index c06c2641..00000000 --- a/scripts/on-build.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# this script is triggered on build -# it writes the environment variables to the config files -# this is needed as nuxtjs replaces all mentions of process.env -# with static vars at build time but we need them fresh at deploy time - -# set the build datetime -export BUILD_DATE="$(date +'%Y-%m-%d %T')" - -# create .env from .env.tmp and fill in build vars -# while swill keep deploy time variables intact -envsub --protect .env.tmp .env -envsub --protect server/.env-secrets.tmp server/.env-secrets diff --git a/scripts/on-dev.sh b/scripts/on-dev.sh deleted file mode 100644 index 45013694..00000000 --- a/scripts/on-dev.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -sh ./scripts/on-build.sh -sh ./scripts/on-start.sh diff --git a/scripts/on-start.sh b/scripts/on-start.sh deleted file mode 100644 index f00bb2b8..00000000 --- a/scripts/on-start.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env ash - -# this script is triggered always when the image is started -# it writes the environment variables to the config files -# this is needed as nuxtjs replaces all mentions of process.env -# with static vars at build time but we need them fresh at deploy time - -# set the deploy datetime -export DEPLOY_DATE="$(date +'%Y-%m-%d %T')" - -# replace the remaining deploy time variables inside .env -# while also setting all non existing vars to empty values -envsub .env .env \ - --system \ - -e WEBAPP_BASE_URL=http://localhost:3000 \ - -e WEBAPP_HOST=localhost \ - -e WEBAPP_PORT=3000 \ - -e API_HOST=localhost \ - -e API_PORT=3030 \ - -e EMBED_API_URL=http://localhost:3050 \ - -e MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ -# empty remaining -envsub .env .env -envsub server/.env-secrets.tmp server/.env-secrets diff --git a/scripts/run-local.sh b/scripts/run-local.sh deleted file mode 100644 index e47a9a1b..00000000 --- a/scripts/run-local.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -# start app on the local network -LOCAL_IP=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'); -env WEBAPP_HOST=$LOCAL_IP env WEBAPP_BASE_URL=http://$LOCAL_IP:3000 env API_HOST=$LOCAL_IP yarn dev diff --git a/server/.env-secrets.tmp b/server/.env-secrets.tmp deleted file mode 100644 index 91da751d..00000000 --- a/server/.env-secrets.tmp +++ /dev/null @@ -1,2 +0,0 @@ -SENTRY_DNS_PRIVATE = ${SENTRY_DNS_PRIVATE} -EMBED_API_TOKEN = ${EMBED_API_TOKEN} \ No newline at end of file diff --git a/server/index.js b/server/index.js index dc00508e..28dfcf9d 100644 --- a/server/index.js +++ b/server/index.js @@ -9,10 +9,8 @@ import redirectSSL from 'redirect-ssl' import avatar from './avatar' import embeds from './embeds' import raven from '../plugins/raven-server' -import { readFileSync } from 'fs' -// Get .env config -require('dotenv').config() +require('dotenv').config() // load .env into process.env const app = express() @@ -43,9 +41,7 @@ app.use('/embeds', embeds()) // Init Nuxt.js const nuxt = new Nuxt(nuxtConfig) -const env = require('dotenv').parse(readFileSync('./.env')) - -app.set('port', env.WEBAPP_PORT) +app.set('port', process.env.WEBAPP_PORT) // Build only in dev mode if (nuxtConfig.dev) { @@ -56,11 +52,11 @@ if (nuxtConfig.dev) { // Give nuxt middleware to express app.use(nuxt.render) // Listen the server -app.listen(env.WEBAPP_PORT, env.WEBAPP_HOST) -console.log(`Server listening on ${env.WEBAPP_HOST}:${env.WEBAPP_PORT}`) // eslint-disable-line no-console -console.log(`MAINTENANCE ${(Boolean(env.MAINTENANCE) === true).toString()}`) // eslint-disable-line no-console -console.log(`WEBAPP_PORT ${env.WEBAPP_PORT}`) // eslint-disable-line no-console -console.log(`WEBAPP_HOST ${env.WEBAPP_HOST}`) // eslint-disable-line no-console -console.log(`WEBAPP_BASE_URL ${env.WEBAPP_BASE_URL}`) // eslint-disable-line no-console -console.log(`API_PORT ${env.API_PORT}`) // eslint-disable-line no-console -console.log(`API_HOST ${env.API_HOST}`) // eslint-disable-line no-console +app.listen(process.env.WEBAPP_PORT, process.env.WEBAPP_HOST) +console.log(`Server listening on ${process.env.WEBAPP_HOST}:${process.env.WEBAPP_PORT}`) // eslint-disable-line no-console +console.log(`MAINTENANCE ${(Boolean(process.env.MAINTENANCE) === true).toString()}`) // eslint-disable-line no-console +console.log(`WEBAPP_PORT ${process.env.WEBAPP_PORT}`) // eslint-disable-line no-console +console.log(`WEBAPP_HOST ${process.env.WEBAPP_HOST}`) // eslint-disable-line no-console +console.log(`WEBAPP_BASE_URL ${process.env.WEBAPP_BASE_URL}`) // eslint-disable-line no-console +console.log(`API_PORT ${process.env.API_PORT}`) // eslint-disable-line no-console +console.log(`API_HOST ${process.env.API_HOST}`) // eslint-disable-line no-console diff --git a/store/auth.js b/store/auth.js index 606b8ba6..69795d09 100644 --- a/store/auth.js +++ b/store/auth.js @@ -1,265 +1,263 @@ import { isEmpty } from 'lodash' -export const state = () => { - return { +export default { + namespaced: true, + state: { user: null, token: null - } -} - -export const mutations = { - SET_USER (state, user) { - state.user = user || null }, - SET_USER_SETTINGS (state, userSettings) { - state.user = Object.assign(state.user, { - userSettings: Object.assign(this.getters['auth/userSettings'], userSettings) - }) + mutations: { + SET_USER (state, user) { + state.user = user || null + }, + SET_USER_SETTINGS (state, userSettings) { + state.user = Object.assign(state.user, { + userSettings: Object.assign(this.getters['auth/userSettings'], userSettings) + }) + }, + SET_TOKEN (state, token) { + state.token = token || null + } }, - SET_TOKEN (state, token) { - state.token = token || null - } -} + getters: { + isAuthenticated (state) { + return !!(state.user && state.token) + }, + isVerified (state) { + return !!state.user && state.user.isVerified && !!state.user.name + }, + isAdmin (state) { + return !!state.user && state.user.role === 'admin' + }, + isModerator (state) { + return !!state.user && (state.user.role === 'admin' || state.user.role === 'moderator') + }, + user (state) { + return state.user + }, + token (state) { + return state.token + }, + userSettings (state, getters, rootState, rootGetters) { + const userSettings = (state.user && state.user.userSettings) ? state.user.userSettings : {} -export const getters = { - isAuthenticated (state) { - return !!(state.user && state.token) - }, - isVerified (state) { - return !!state.user && state.user.isVerified && !!state.user.name - }, - isAdmin (state) { - return !!state.user && state.user.role === 'admin' - }, - isModerator (state) { - return !!state.user && (state.user.role === 'admin' || state.user.role === 'moderator') - }, - user (state) { - return state.user - }, - token (state) { - return state.token - }, - userSettings (state, getters, rootState, rootGetters) { - const userSettings = (state.user && state.user.userSettings) ? state.user.userSettings : {} + const defaultLanguage = (state.user && state.user.language) ? state.user.language : rootGetters['i18n/locale'] + let contentLanguages = !isEmpty(userSettings.contentLanguages) ? userSettings.contentLanguages : [] + if (isEmpty(contentLanguages)) { + contentLanguages = userSettings.uiLanguage ? [userSettings.uiLanguage] : [defaultLanguage] + } - const defaultLanguage = (state.user && state.user.language) ? state.user.language : rootGetters['i18n/locale'] - let contentLanguages = !isEmpty(userSettings.contentLanguages) ? userSettings.contentLanguages : [] - if (isEmpty(contentLanguages)) { - contentLanguages = userSettings.uiLanguage ? [userSettings.uiLanguage] : [defaultLanguage] + return Object.assign({ + uiLanguage: defaultLanguage, + contentLanguages: contentLanguages + }, userSettings) } + }, + actions: { + async init ({commit, dispatch, state}) { + let user + // get fresh jwt token + await dispatch('refreshJWT', 'auth/init') - return Object.assign({ - uiLanguage: defaultLanguage, - contentLanguages: contentLanguages - }, userSettings) - } -} + // check if the token is authenticated + const isAuthenticated = await dispatch('checkAuth') + if (isAuthenticated) { + try { + const payload = await this.app.$api.passport.verifyJWT(state.token) + user = await this.app.$api.service('users').get(payload.userId) + } catch (err) { + user = null + } + commit('SET_USER', user) + } + return user + }, + async refreshJWT ({state, getters, commit, dispatch}, source = null) { + const token = await this.app.$api.passport.getJWT() + commit('SET_TOKEN', token) -export const actions = { - async init ({commit, dispatch, state}) { - let user - // get fresh jwt token - await dispatch('refreshJWT', 'auth/init') + if (process.env.NODE_ENV === 'development') { + console.log('### refreshJWT', source, !isEmpty(token), getters.isAuthenticated) + } - // check if the token is authenticated - const isAuthenticated = await dispatch('checkAuth') - if (isAuthenticated) { - try { - const payload = await this.app.$api.passport.verifyJWT(state.token) - user = await this.app.$api.service('users').get(payload.userId) - } catch (err) { - user = null + let user + if (token) { + try { + user = await this.app.$api.auth({strategy: 'jwt', accessToken: token}) + } catch (err) { + user = null + } + commit('SET_USER', user) + } + return user + }, + async checkAuth ({state, getters, commit, dispatch}) { + if (!getters.user) { + commit('SET_USER', null) + commit('SET_TOKEN', null) + return false } - commit('SET_USER', user) - } - return user - }, - async refreshJWT ({state, getters, commit, dispatch}, source = null) { - const token = await this.app.$api.passport.getJWT() - commit('SET_TOKEN', token) - if (process.env.NODE_ENV === 'development') { - console.log('### refreshJWT', source, !isEmpty(token), getters.isAuthenticated) - } + const token = await this.app.$api.passport.getJWT() + commit('SET_TOKEN', token) - let user - if (token) { + if (!token) { + return false + } + let payloadValid = false try { - user = await this.app.$api.auth({strategy: 'jwt', accessToken: token}) + const payload = await this.app.$api.passport.verifyJWT(token) + payloadValid = this.app.$api.passport.payloadIsValid(payload) } catch (err) { - user = null + payloadValid = false + } + if (payloadValid) { + commit('SET_TOKEN', token) + } else { + commit('SET_USER', null) + commit('SET_TOKEN', null) } - commit('SET_USER', user) - } - return user - }, - async checkAuth ({state, getters, commit, dispatch}) { - if (!getters.user) { - commit('SET_USER', null) - commit('SET_TOKEN', null) - return false - } - - const token = await this.app.$api.passport.getJWT() - commit('SET_TOKEN', token) - - if (!token) { - return false - } - let payloadValid = false - try { - const payload = await this.app.$api.passport.verifyJWT(token) - payloadValid = this.app.$api.passport.payloadIsValid(payload) - } catch (err) { - payloadValid = false - } - if (payloadValid) { - commit('SET_TOKEN', token) - } else { - commit('SET_USER', null) - commit('SET_TOKEN', null) - } - return payloadValid === true - }, - async login ({commit, dispatch, getters}, {email, password}) { - try { - commit('SET_USER', null) - commit('SET_TOKEN', null) - const user = await this.app.$api.auth({strategy: 'local', email, password}) + return payloadValid === true + }, + async login ({commit, dispatch, getters}, {email, password}) { + try { + commit('SET_USER', null) + commit('SET_TOKEN', null) + const user = await this.app.$api.auth({strategy: 'local', email, password}) - commit('SET_USER', user) + commit('SET_USER', user) - try { - const waitForUserSettings = () => { - return new Promise(resolve => { - setTimeout(() => { - resolve(getters.userSettings) - }, 250) - }) - } - const locale = this.app.$cookies.get('locale') - const userSettings = await waitForUserSettings() - if (!isEmpty(locale) && user && userSettings && userSettings.uiLanguage !== locale) { - // console.log('update user locale setting with cookie setting (had changed in on login screen)') - // update user locale setting with cookie setting (had changed in on login screen) - dispatch('usersettings/patch', { - uiLanguage: locale - }, { root: true }) - } else if (isEmpty(locale) && user && userSettings && userSettings.uiLanguage) { - // console.log('set locale to user setting and persist in cookie') - // set locale to user setting and persist in cookie - this.app.$cookies.set('locale', userSettings.uiLanguage) - this.app.$i18n.set(userSettings.uiLanguage) + try { + const waitForUserSettings = () => { + return new Promise(resolve => { + setTimeout(() => { + resolve(getters.userSettings) + }, 250) + }) + } + const locale = this.app.$cookies.get('locale') + const userSettings = await waitForUserSettings() + if (!isEmpty(locale) && user && userSettings && userSettings.uiLanguage !== locale) { + // console.log('update user locale setting with cookie setting (had changed in on login screen)') + // update user locale setting with cookie setting (had changed in on login screen) + dispatch('usersettings/patch', { + uiLanguage: locale + }, { root: true }) + } else if (isEmpty(locale) && user && userSettings && userSettings.uiLanguage) { + // console.log('set locale to user setting and persist in cookie') + // set locale to user setting and persist in cookie + this.app.$cookies.set('locale', userSettings.uiLanguage) + this.app.$i18n.set(userSettings.uiLanguage) + } + commit('newsfeed/clear', null, { root: true }) + } catch (err) { + console.error(err) } - commit('newsfeed/clear', null, { root: true }) + + return user } catch (err) { - console.error(err) + commit('SET_USER', null) + // commit('SET_TOKEN', null) + throw new Error(err.message) } - - return user - } catch (err) { - commit('SET_USER', null) - // commit('SET_TOKEN', null) - throw new Error(err.message) - } - }, - async logout ({commit}) { - this.app.router.push('/auth/logout') - }, - register ({dispatch, commit}, {email, password, inviteCode, invitedByUserId}) { - return this.app.$api.service('users').create({email, password, inviteCode, invitedByUserId, termsAndConditionsAccepted: new Date()}) - .then(response => { - return dispatch('login', {email, password}) - }) - }, - async patch ({state, commit, dispatch}, data) { - let user = state.user - // console.log('####################') - // console.log('#AUTH/PATCH', data) - // console.log('#USER ID', state.user._id) - // console.log('#JWT TOKEN', this.$cookies.get(this.app.$api.authKey)) - // console.log('#state.isAuthenticated', state.isAuthenticated) - // console.log('#state.use', state.use) - // console.log('####################') - // if (!state.isAuthenticated || !user) { - // user = await dispatch('refreshJWT') - // } - if (!user) { - // stop when the user is not authenticated - console.error('stop when the user is not authenticated') - throw new Error('NO USER') - } - user = await this.app.$api.service('users').patch(user._id, data) - commit('SET_USER', user) - return user - }, - async refreshUser ({state, commit}, userSettings) { - if (state.user && userSettings) { - commit('SET_USER_SETTINGS', userSettings) - } else if (state.user) { - const user = await this.app.$api.service('users').get(state.user._id) + }, + async logout ({commit}) { + this.app.router.push('/auth/logout') + }, + register ({dispatch, commit}, {email, password, inviteCode, invitedByUserId}) { + return this.app.$api.service('users').create({email, password, inviteCode, invitedByUserId, termsAndConditionsAccepted: new Date()}) + .then(response => { + return dispatch('login', {email, password}) + }) + }, + async patch ({state, commit, dispatch}, data) { + let user = state.user + // console.log('####################') + // console.log('#AUTH/PATCH', data) + // console.log('#USER ID', state.user._id) + // console.log('#JWT TOKEN', this.$cookies.get(this.app.$api.authKey)) + // console.log('#state.isAuthenticated', state.isAuthenticated) + // console.log('#state.use', state.use) + // console.log('####################') + // if (!state.isAuthenticated || !user) { + // user = await dispatch('refreshJWT') + // } + if (!user) { + // stop when the user is not authenticated + console.error('stop when the user is not authenticated') + throw new Error('NO USER') + } + user = await this.app.$api.service('users').patch(user._id, data) commit('SET_USER', user) return user - } - return state.user - }, - verify ({dispatch}, verifyToken) { - if (!verifyToken) { return false } - return this.app.$api.service('authManagement').create({ - action: 'verifySignupLong', - value: verifyToken - }) - .then(() => { - return true - }) - .catch(err => { - console.error(err.message, err) - }) - }, - resendVerifySignup ({state, dispatch}) { - if (!state.user.email) { return false } - return this.app.$api.service('authManagement').create({ - action: 'resendVerifySignup', - value: { - email: state.user.email + }, + async refreshUser ({state, commit}, userSettings) { + if (state.user && userSettings) { + commit('SET_USER_SETTINGS', userSettings) + } else if (state.user) { + const user = await this.app.$api.service('users').get(state.user._id) + commit('SET_USER', user) + return user } - }) - .then(() => { - return true - }) - .catch(err => { - console.error(err.message, err) + return state.user + }, + verify ({dispatch}, verifyToken) { + if (!verifyToken) { return false } + return this.app.$api.service('authManagement').create({ + action: 'verifySignupLong', + value: verifyToken }) - }, - resetPassword ({state}, data) { - return this.app.$api.service('authManagement').create({ - action: 'sendResetPwd', - value: { - email: data.email - } - }) - .then(() => { - return true - }) - .catch(err => { - throw new Error(err.message) + .then(() => { + return true + }) + .catch(err => { + console.error(err.message, err) + }) + }, + resendVerifySignup ({state, dispatch}) { + if (!state.user.email) { return false } + return this.app.$api.service('authManagement').create({ + action: 'resendVerifySignup', + value: { + email: state.user.email + } }) - }, - setNewPassword ({state}, data) { - return this.app.$api.service('authManagement').create({ - action: 'resetPwdLong', - value: { - token: data.token, - password: data.password - } - }) - .then(() => { - return true + .then(() => { + return true + }) + .catch(err => { + console.error(err.message, err) + }) + }, + resetPassword ({state}, data) { + return this.app.$api.service('authManagement').create({ + action: 'sendResetPwd', + value: { + email: data.email + } }) - .catch(err => { - throw new Error(err.message) + .then(() => { + return true + }) + .catch(err => { + throw new Error(err.message) + }) + }, + setNewPassword ({state}, data) { + return this.app.$api.service('authManagement').create({ + action: 'resetPwdLong', + value: { + token: data.token, + password: data.password + } }) + .then(() => { + return true + }) + .catch(err => { + throw new Error(err.message) + }) + } } } diff --git a/store/categories.js b/store/categories.js index 37414ace..68c3b09e 100644 --- a/store/categories.js +++ b/store/categories.js @@ -1,66 +1,66 @@ import { castArray } from 'lodash' -export const state = () => { - return { +export default { + namespaced: true, + state: { categories: [] - } -} - -export const mutations = { - set (state, categories) { - state.categories = castArray(categories) }, - // [vuex] unknown local mutation type: clear, global type: categories/clear - clear (state) { - state.categories = [] - } -} - -export const getters = { - all (state) { - return state.categories - } -} - -export const actions = { - // Called from nuxtServerInit in index - init ({state, dispatch}) { - if (state.categories.length) { - // do not fetch again - return + mutations: { + set (state, categories) { + state.categories = castArray(categories) + }, + // [vuex] unknown local mutation type: clear, global type: categories/clear + clear (state) { + state.categories = [] } - return dispatch('fetch') }, - // Called from plugins/init-store-subscriptions only once - subscribe ({dispatch}) { - return this.app.$api.service('categories') - .on('created', () => { - dispatch('fetch') - }) + + getters: { + all (state) { + return state.categories + } }, - fetch ({commit}) { - return this.app.$api.service('categories').find({ - query: { - '$limit': 200, - '$sort': { - slug: 1 - } + + actions: { + // Called from nuxtServerInit in index + init ({state, dispatch}) { + if (state.categories.length) { + // do not fetch again + return } - }) - .then((result) => { - commit('set', result.data) - }) - .catch(() => { - commit('clear') + return dispatch('fetch') + }, + // Called from plugins/init-store-subscriptions only once + subscribe ({dispatch}) { + return this.app.$api.service('categories') + .on('created', () => { + dispatch('fetch') + }) + }, + fetch ({commit}) { + return this.app.$api.service('categories').find({ + query: { + '$limit': 200, + '$sort': { + slug: 1 + } + } }) - }, - create ({dispatch}, category) { - return this.app.$api.service('categories').create(category) - }, - patch ({dispatch}, category) { - return this.app.$api.service('categories').patch(category._id, category) - }, - delete ({dispatch}, category) { - return this.app.$api.service('categories').remove(category._id) + .then((result) => { + commit('set', result.data) + }) + .catch(() => { + commit('clear') + }) + }, + create ({dispatch}, category) { + return this.app.$api.service('categories').create(category) + }, + patch ({dispatch}, category) { + return this.app.$api.service('categories').patch(category._id, category) + }, + delete ({dispatch}, category) { + return this.app.$api.service('categories').remove(category._id) + } } } diff --git a/store/comments.js b/store/comments.js index 9d863a43..204768ae 100644 --- a/store/comments.js +++ b/store/comments.js @@ -1,98 +1,96 @@ import { castArray, debounce } from 'lodash' -export const state = () => { - return { +export default { + namespaced: true, + state: { comments: [], isLoading: true, contributionId: null - } -} - -export const mutations = { - isLoading (state, status) { - state.isLoading = status }, - set (state, comments) { - state.comments = castArray(comments) - }, - clear (state) { - state.comments = [] - }, - setContributionId (state, contributionId) { - state.contributionId = contributionId - } -} - -export const getters = { - all (state) { - return state.comments - }, - isLoading (state) { - return state.isLoading - }, - count (state) { - return state.comments.length - } -} - -export const actions = { - // Called from plugins/init-store-subscriptions only once - subscribe ({dispatch}) { - return this.app.$api.service('comments') - .on('created', debounce((comment) => { - dispatch('fetchByContributionId') - }, 500)) - .on('patched', debounce((comment) => { - dispatch('fetchByContributionId') - }, 500)) - .on('removed', debounce((comment) => { - dispatch('fetchByContributionId') - }, 500)) + mutations: { + isLoading (state, status) { + state.isLoading = status + }, + set (state, comments) { + state.comments = castArray(comments) + }, + clear (state) { + state.comments = [] + }, + setContributionId (state, contributionId) { + state.contributionId = contributionId + } }, - fetchByContributionId ({commit, state}, contributionId) { - contributionId = contributionId || state.contributionId - if (!contributionId) { - return + getters: { + all (state) { + return state.comments + }, + isLoading (state) { + return state.isLoading + }, + count (state) { + return state.comments.length } - commit('setContributionId', contributionId) - // TODO: implement pagination for comments - return this.app.$api.service('comments').find({ - query: { - contributionId: contributionId, - $sort: { - // upvoteCount: -1, - createdAt: 1 - }, - $limit: 100 + }, + actions: { + // Called from plugins/init-store-subscriptions only once + subscribe ({dispatch}) { + return this.app.$api.service('comments') + .on('created', debounce((comment) => { + dispatch('fetchByContributionId') + }, 500)) + .on('patched', debounce((comment) => { + dispatch('fetchByContributionId') + }, 500)) + .on('removed', debounce((comment) => { + dispatch('fetchByContributionId') + }, 500)) + }, + fetchByContributionId ({commit, state}, contributionId) { + contributionId = contributionId || state.contributionId + if (!contributionId) { + return } - }) - .then((result) => { - commit('set', result.data) - commit('isLoading', false) + commit('setContributionId', contributionId) + // TODO: implement pagination for comments + return this.app.$api.service('comments').find({ + query: { + contributionId: contributionId, + $sort: { + // upvoteCount: -1, + createdAt: 1 + }, + $limit: 100 + } }) - .catch((e) => { - commit('isLoading', false) + .then((result) => { + commit('set', result.data) + commit('isLoading', false) + }) + .catch((e) => { + commit('isLoading', false) + }) + }, + fetchById ({commit}, id) { + return this.app.$api.service('comments').get(id) + }, + upvote ({dispatch}, comment) { + return this.app.$api.service('comments').patch(comment._id, { + $inc: { + upvoteCount: 1 + } + }).then((res) => { + dispatch('fetchByContributionId', comment.contributionId) }) - }, - fetchById ({commit}, id) { - return this.app.$api.service('comments').get(id) - }, - upvote ({dispatch}, comment) { - return this.app.$api.service('comments').patch(comment._id, { - $inc: { - upvoteCount: 1 - } - }).then((res) => { - dispatch('fetchByContributionId', comment.contributionId) - }) - }, - create ({dispatch}, data) { - return this.app.$api.service('comments').create(data) - }, - patch ({dispatch}, data) { - return this.app.$api.service('comments').patch(data._id, data) - }, - remove ({dispatch}, id) { - return this.app.$api.service('comments').remove(id) + }, + create ({dispatch}, data) { + return this.app.$api.service('comments').create(data) + }, + patch ({dispatch}, data) { + return this.app.$api.service('comments').patch(data._id, data) + }, + remove ({dispatch}, id) { + return this.app.$api.service('comments').remove(id) + } } } diff --git a/store/connections.js b/store/connections.js index ee5beba9..6488fc30 100644 --- a/store/connections.js +++ b/store/connections.js @@ -1,5 +1,6 @@ -export const state = () => { - return { +export default { + namespaced: true, + state: { follow: { _id: null, userId: null, @@ -9,104 +10,104 @@ export const state = () => { isFollowing: false, count: 0 } - } -} - -export const mutations = { - follow (state, follow) { - state.follow = follow - } -} - -export const getters = { - follow (state) { - return state.follow - } -} + }, -export const actions = { - async syncFollow ({state, commit}, {userId, foreignId, foreignService}) { - let status = Object.assign({}, state.follow) + mutations: { + follow (state, follow) { + state.follow = follow + } + }, - if (status.userId !== userId) { - status.count = 0 + getters: { + follow (state) { + return state.follow } - status.userId = userId - status.foreignId = foreignId - status.foreignService = foreignService - status.isPending = true - commit('follow', status) - - status = Object.assign({}, state.follow) - - try { - const res = await this.app.$api.service('follows').find({ - query: { - userId, + }, + + actions: { + async syncFollow ({state, commit}, {userId, foreignId, foreignService}) { + let status = Object.assign({}, state.follow) + + if (status.userId !== userId) { + status.count = 0 + } + status.userId = userId + status.foreignId = foreignId + status.foreignService = foreignService + status.isPending = true + commit('follow', status) + + status = Object.assign({}, state.follow) + + try { + const res = await this.app.$api.service('follows').find({ + query: { + userId, + foreignId, + foreignService + } + }) + status.count = res.total || 0 + status._id = res.data.length ? res.data[0]._id : null + } catch (err) { + console.error(err) + status.count = 0 + } + + status.isFollowing = Boolean(status._id) + status.isPending = false + + commit('follow', status) + }, + async follow ({state, commit, dispatch}, {foreignId, foreignService}) { + let status = Object.assign({}, state.follow) + status.isPending = true + commit('follow', status) + + status = Object.assign({}, state.follow) + + try { + await this.app.$api.service('follows').create({ foreignId, foreignService - } - }) - status.count = res.total || 0 - status._id = res.data.length ? res.data[0]._id : null - } catch (err) { - console.error(err) - status.count = 0 - } + }) + } catch (err) {} - status.isFollowing = Boolean(status._id) - status.isPending = false + status.isFollowing = true + // status.isPending = false - commit('follow', status) - }, - async follow ({state, commit, dispatch}, {foreignId, foreignService}) { - let status = Object.assign({}, state.follow) - status.isPending = true - commit('follow', status) - - status = Object.assign({}, state.follow) + commit('follow', status) - try { - await this.app.$api.service('follows').create({ - foreignId, - foreignService + dispatch('syncFollow', { + userId: state.follow.userId, + foreignId: foreignId, + foreignService: foreignService }) - } catch (err) {} - - status.isFollowing = true - // status.isPending = false - - commit('follow', status) - - dispatch('syncFollow', { - userId: state.follow.userId, - foreignId: foreignId, - foreignService: foreignService - }) - }, - async unfollow ({state, commit, dispatch}, {_id}) { - let status = Object.assign({}, state.follow) - status.isPending = true - commit('follow', status) - - status = Object.assign({}, state.follow) - - try { - await this.app.$api.service('follows').remove({ - _id: _id + }, + async unfollow ({state, commit, dispatch}, {_id}) { + let status = Object.assign({}, state.follow) + status.isPending = true + commit('follow', status) + + status = Object.assign({}, state.follow) + + try { + await this.app.$api.service('follows').remove({ + _id: _id + }) + } catch (err) {} + + status.isFollowing = false + status.count-- + // status.isPending = false + status._id = null + commit('follow', status) + + dispatch('syncFollow', { + userId: state.follow.userId, + foreignId: state.follow.foreignId, + foreignService: state.follow.foreignService }) - } catch (err) {} - - status.isFollowing = false - status.count-- - // status.isPending = false - status._id = null - commit('follow', status) - - dispatch('syncFollow', { - userId: state.follow.userId, - foreignId: state.follow.foreignId, - foreignService: state.follow.foreignService - }) + } } } diff --git a/store/env.js b/store/env.js deleted file mode 100644 index a1f59a5f..00000000 --- a/store/env.js +++ /dev/null @@ -1,25 +0,0 @@ -export const state = () => { - return { - env: {} - } -} - -export const mutations = { - SET_ENV (state, env) { - state.env = env - } -} - -export const getters = { - all (state) { - return state.env - } -} - -export const actions = { - init ({commit}) { - if (process.server) { - commit('SET_ENV', require('dotenv').config().parsed) - } - } -} diff --git a/store/index.js b/store/index.js index ffcab2a1..fb5b10ae 100644 --- a/store/index.js +++ b/store/index.js @@ -1,7 +1,67 @@ -export const actions = { - async nuxtServerInit ({dispatch}) { - dispatch('categories/init') - await dispatch('auth/init') - await dispatch('settings/init') - } +import feathersVuex, { initAuth } from 'feathers-vuex' +import createApiClient from '../helpers/createApiClient' +import Vuex from 'vuex' + +import auth from './auth' +import categories from './categories' +import comments from './comments' +import connections from './connections' +import layout from './layout' +import newsfeed from './newsfeed' +import notifications from './notifications' +import organizations from './organizations' +import search from './search' +import settings from './settings' +import usersettings from './usersettings' + +const requireModule = require.context( + // The relative path holding the service modules + './services', + // Whether to look in subfolders + false, + // Only include .js files (prevents duplicate imports) + /.js$/ +) + +const createStore = (ssrContext) => { + const feathersClient = createApiClient(ssrContext || {}) + const { auth: feathersVuexAuthentication } = feathersVuex(feathersClient, { idField: '_id' }) + const servicePlugins = requireModule.keys().map(modulePath => requireModule(modulePath).default(feathersClient)) + + return new Vuex.Store({ + modules: { auth, categories, comments, connections, layout, newsfeed, notifications, organizations, search, settings, usersettings }, + actions: { + async nuxtServerInit ({dispatch, commit}, {req}) { + dispatch('categories/init') + await dispatch('auth/init') + await dispatch('settings/init') + return initAuth({ + commit, + dispatch, + req, + moduleName: 'auth', + cookieName: 'feathers-jwt' + }) + } + }, + plugins: [ + ...servicePlugins, + feathersVuexAuthentication({ + userService: 'users', + state: { + publicPages: [ + 'auth-login', + 'auth-register', + 'auth-signup', + 'auth-reset', + 'auth-reset-token', + 'pages-slug', + 'test' + ] + } + }) + ] + }) } + +export default createStore diff --git a/store/layout.js b/store/layout.js index d3e96e70..377d9bbd 100644 --- a/store/layout.js +++ b/store/layout.js @@ -1,47 +1,48 @@ -export const state = () => { - return { +export default { + namespaced: true, + state: { change: 0, sidebar: { open: false } - } -} - -export const mutations = { - CHANGE_LAYOUT (state) { - state.change++ - }, - CLOSE_SIDEBAR (state) { - state.sidebar.open = false }, - OPEN_SIDEBAR (state) { - state.sidebar.open = true - }, - TOGGLE_SIDEBAR (state) { - state.sidebar.open = !state.sidebar.open - } -} -export const getters = { - change (state) { - return state.change + mutations: { + CHANGE_LAYOUT (state) { + state.change++ + }, + CLOSE_SIDEBAR (state) { + state.sidebar.open = false + }, + OPEN_SIDEBAR (state) { + state.sidebar.open = true + }, + TOGGLE_SIDEBAR (state) { + state.sidebar.open = !state.sidebar.open + } }, - sidebar (state) { - return state.sidebar - } -} -export const actions = { - change ({commit}) { - commit('CHANGE_LAYOUT') - }, - closeSidebar ({commit}) { - commit('CLOSE_SIDEBAR') - }, - openSidebar ({commit}) { - commit('OPEN_SIDEBAR') + getters: { + change (state) { + return state.change + }, + sidebar (state) { + return state.sidebar + } }, - toggleSidebar ({commit}) { - commit('TOGGLE_SIDEBAR') + + actions: { + change ({commit}) { + commit('CHANGE_LAYOUT') + }, + closeSidebar ({commit}) { + commit('CLOSE_SIDEBAR') + }, + openSidebar ({commit}) { + commit('OPEN_SIDEBAR') + }, + toggleSidebar ({commit}) { + commit('TOGGLE_SIDEBAR') + } } } diff --git a/store/newsfeed.js b/store/newsfeed.js index 3b9fc4dc..1b08981f 100644 --- a/store/newsfeed.js +++ b/store/newsfeed.js @@ -2,8 +2,9 @@ import Vue from 'vue' import _ from 'lodash' import { Base64 } from 'js-base64' -export const state = () => { - return { +export default { + namespaced: true, + state: { contributions: [], search: null, filter: { @@ -22,194 +23,194 @@ export const state = () => { lastQueryDate: null, hasNext: false, lastScrollPos: null - } -} - -export const mutations = { - setLoading (state, isLoading) { - state.isLoading = isLoading - }, - movePaginationCursor (state) { - if (state.contributions.length) { - state.skip = state.skip + state.limit - } else { - state.skip = 0 - } - }, - setLastQueryHash (state, queryHash) { - if (state.lastQueryHash !== queryHash) { - state.lastQueryDate = new Date() - } - state.lastQueryHash = queryHash - }, - setSearch (state, value) { - state.search = value - }, - setFilter (state, value) { - state.filter = value - }, - addContributions (state, contributions) { - state.contributions = _.uniqBy(state.contributions.concat(contributions), '_id') }, - updateContribution (state, contribution) { - const index = _.findIndex(state.contributions, { _id: contribution._id }) - if (index >= 0) { - if (contribution.deleted) { - Vue.delete(state.contributions, index) + + mutations: { + setLoading (state, isLoading) { + state.isLoading = isLoading + }, + movePaginationCursor (state) { + if (state.contributions.length) { + state.skip = state.skip + state.limit } else { - Vue.set(state.contributions, index, contribution) + state.skip = 0 } - } - }, - setHasNext (state, hasNext) { - state.hasNext = hasNext - }, - setLastScrollPos (state, scrollPos) { - state.lastScrollPos = scrollPos - }, - setSortField (state, sortField) { - state.sortField = sortField - state.sort = {} - state.sort[state.sortField] = (state.sortOrder.toLowerCase() === 'desc') ? -1 : 1 - }, - setSortOrder (state, order) { - state.sortOrder = order - state.sort = {} - state.sort[state.sortField] = (state.sortOrder.toLowerCase() === 'desc') ? -1 : 1 - }, - clear (state) { - state.contributions = [] - state.skip = 0 - state.isLoading = false - state.hasNext = true - state.lastQueryHash = null - state.lastScrollPos = null - } -} - -export const getters = { - all (state) { - return state.contributions - }, - sortField (state) { - return state.sortField - }, - sortOrder (state) { - return state.sortOrder - }, - getCurrentQueryHash (state, getters, rootState, rootGetters) { - let queryData = { - search: state.search, - filter: state.filter, - limit: state.limit, - skip: state.skip, - sort: state.sort - } - if (!_.isEmpty(state.search)) { - queryData.$sort = {} - } - if (rootState.auth.user) { - queryData.language = { - $in: _.castArray(rootGetters['auth/userSettings'].contentLanguages) + }, + setLastQueryHash (state, queryHash) { + if (state.lastQueryHash !== queryHash) { + state.lastQueryDate = new Date() + } + state.lastQueryHash = queryHash + }, + setSearch (state, value) { + state.search = value + }, + setFilter (state, value) { + state.filter = value + }, + addContributions (state, contributions) { + state.contributions = _.uniqBy(state.contributions.concat(contributions), '_id') + }, + updateContribution (state, contribution) { + const index = _.findIndex(state.contributions, { _id: contribution._id }) + if (index >= 0) { + if (contribution.deleted) { + Vue.delete(state.contributions, index) + } else { + Vue.set(state.contributions, index, contribution) + } } + }, + setHasNext (state, hasNext) { + state.hasNext = hasNext + }, + setLastScrollPos (state, scrollPos) { + state.lastScrollPos = scrollPos + }, + setSortField (state, sortField) { + state.sortField = sortField + state.sort = {} + state.sort[state.sortField] = (state.sortOrder.toLowerCase() === 'desc') ? -1 : 1 + }, + setSortOrder (state, order) { + state.sortOrder = order + state.sort = {} + state.sort[state.sortField] = (state.sortOrder.toLowerCase() === 'desc') ? -1 : 1 + }, + clear (state) { + state.contributions = [] + state.skip = 0 + state.isLoading = false + state.hasNext = true + state.lastQueryHash = null + state.lastScrollPos = null } - const hash = JSON.stringify(queryData) - return Base64.encode(hash) - }, - isLoading (state) { - return state.isLoading - }, - hasNext (state) { - return state.hasNext }, - lastScrollPos (state) { - return state.lastScrollPos - }, - getCurrentQuery (state, getters, rootState, rootGetters) { - let query = { - $skip: state.skip, - $limit: state.limit, - $sort: state.sort, - visibility: 'public' - } - if (!_.isEmpty(state.search)) { - query.$sort = {} - } - query = Object.assign(query, rootGetters['search/queryLanguages']) - // generate the search query with the token entered inside the search field - if (!_.isEmpty(state.search)) { - // query.title = { $search: state.search } - query.$search = state.search - query.$language = Vue.i18n.locale() - } - // generate the category filter query by using the selected category ids - query = Object.assign(query, rootGetters['search/queryCategories']) + getters: { + all (state) { + return state.contributions + }, + sortField (state) { + return state.sortField + }, + sortOrder (state) { + return state.sortOrder + }, + getCurrentQueryHash (state, getters, rootState, rootGetters) { + let queryData = { + search: state.search, + filter: state.filter, + limit: state.limit, + skip: state.skip, + sort: state.sort + } + if (!_.isEmpty(state.search)) { + queryData.$sort = {} + } + if (rootState.auth.user) { + queryData.language = { + $in: _.castArray(rootGetters['auth/userSettings'].contentLanguages) + } + } + const hash = JSON.stringify(queryData) + return Base64.encode(hash) + }, + isLoading (state) { + return state.isLoading + }, + hasNext (state) { + return state.hasNext + }, + lastScrollPos (state) { + return state.lastScrollPos + }, + getCurrentQuery (state, getters, rootState, rootGetters) { + let query = { + $skip: state.skip, + $limit: state.limit, + $sort: state.sort, + visibility: 'public' + } + if (!_.isEmpty(state.search)) { + query.$sort = {} + } + query = Object.assign(query, rootGetters['search/queryLanguages']) - // generate the emotions filter query by using the selected emotions - query = Object.assign(query, rootGetters['search/queryEmotions']) + // generate the search query with the token entered inside the search field + if (!_.isEmpty(state.search)) { + // query.title = { $search: state.search } + query.$search = state.search + query.$language = Vue.i18n.locale() + } + // generate the category filter query by using the selected category ids + query = Object.assign(query, rootGetters['search/queryCategories']) - return query - } -} + // generate the emotions filter query by using the selected emotions + query = Object.assign(query, rootGetters['search/queryEmotions']) -export const actions = { - // Called from plugins/init-store-subscriptions only once - subscribe ({state, commit}) { - return this.app.$api.service('contributions') - .on('patched', (res) => { - commit('updateContribution', res) - }) - }, - /** - * fetch all contributions for the given query and filter settings - */ - async fetch ({state, getters, commit}) { - if (state.isLoading === true) { - // console.log('FETCH CANCELED AS THERE IS SOMETHING LOADING...') - return + return query } - commit('setLoading', true) + }, - // return current data if query is the same like the last one - const queryHash = getters.getCurrentQueryHash - if (!_.isEmpty(queryHash) && queryHash === state.lastQueryHash && state.contributions.length) { - // console.log('#LOAD FROM CACHE') - setTimeout(() => { - commit('setLoading', false) - }, 150) - return state.contributions - } + actions: { + // Called from plugins/init-store-subscriptions only once + subscribe ({state, commit}) { + return this.app.$api.service('contributions') + .on('patched', (res) => { + commit('updateContribution', res) + }) + }, + /** + * fetch all contributions for the given query and filter settings + */ + async fetch ({state, getters, commit}) { + if (state.isLoading === true) { + // console.log('FETCH CANCELED AS THERE IS SOMETHING LOADING...') + return + } + commit('setLoading', true) + + // return current data if query is the same like the last one + const queryHash = getters.getCurrentQueryHash + if (!_.isEmpty(queryHash) && queryHash === state.lastQueryHash && state.contributions.length) { + // console.log('#LOAD FROM CACHE') + setTimeout(() => { + commit('setLoading', false) + }, 150) + return state.contributions + } - const query = getters.getCurrentQuery + const query = getters.getCurrentQuery - commit('setLastQueryHash', queryHash) + commit('setLastQueryHash', queryHash) - try { - const res = await this.app.$api.service('contributions').find({query}) - commit('addContributions', res.data) + try { + const res = await this.app.$api.service('contributions').find({query}) + commit('addContributions', res.data) - setTimeout(() => { - const lastItemNum = res.data.length + res.skip - commit('setHasNext', lastItemNum < res.total) setTimeout(() => { - commit('setLoading', false) - }, 150) - }, 500) - } catch (err) { - commit('setLoading', false) - throw new Error(500, err.message) - } - }, - /** - * load more entries for the given query and filter settings - */ - fetchMore ({state, dispatch, commit}) { - if (state.isLoading === true) { - // console.log('FETCHMORE CANCELED AS THERE IS SOMETHING LOADING...') - return + const lastItemNum = res.data.length + res.skip + commit('setHasNext', lastItemNum < res.total) + setTimeout(() => { + commit('setLoading', false) + }, 150) + }, 500) + } catch (err) { + commit('setLoading', false) + throw new Error(500, err.message) + } + }, + /** + * load more entries for the given query and filter settings + */ + fetchMore ({state, dispatch, commit}) { + if (state.isLoading === true) { + // console.log('FETCHMORE CANCELED AS THERE IS SOMETHING LOADING...') + return + } + commit('movePaginationCursor') + dispatch('fetch') } - commit('movePaginationCursor') - dispatch('fetch') } } diff --git a/store/notifications.js b/store/notifications.js index bec5b94b..30dca5b8 100644 --- a/store/notifications.js +++ b/store/notifications.js @@ -4,237 +4,238 @@ const options = { limit: 10 } -export const state = () => { - return { +export default { + namespaced: true, + state: { total: 0, unseenTotal: 0, onlyUnseen: true, notifications: false, isLoading: false - } -} - -export const mutations = { - total (state, total) { - state.total = total - }, - unseenTotal (state, unseenTotal) { - state.unseenTotal = unseenTotal - }, - set (state, notifications) { - if (!notifications || notifications === undefined) { - state.notifications = null - } else { - state.notifications = notifications - } - }, - setOnlyUnseen (state, val) { - state.onlyUnseen = val - }, - clear (state) { - state.total = 0 - state.unseenTotal = 0 - state.notifications = [] - }, - add (state, notification) { - let toBottom = false - if (notification.toBottom) { - // to an object with more options, unwrap it... - toBottom = true - notification = notification.notification - } - const index = state.notifications.findIndex(item => { - return item._id === notification._id - }) - if (index > -1) { - // Replace existing notification - state.notifications.splice(index, 1, notification) - } else if (toBottom) { - // Or add new one - state.notifications.push(notification) - } else { - // Or add new one - state.notifications.unshift(notification) - } - }, - isLoading (state, isLoading) { - state.isLoading = isLoading - } -} - -export const getters = { - all (state) { - return state.notifications - }, - total (state) { - return state.total - }, - unseenTotal (state) { - return state.unseenTotal - }, - onlyUnseen (state) { - return state.onlyUnseen }, - hasMore (state) { - const total = state.onlyUnseen ? state.unseenTotal : state.total - return total && - total > state.notifications.length - }, - isLoading (state) { - return state.isLoading - } -} -export const actions = { - // Called from nuxtServerInit in index - init ({dispatch}) { - return dispatch('fetch') - }, - // Called from plugins/init-store-subscriptions only once - subscribe ({dispatch}) { - return this.app.$api.service('notifications') - .on('created', async (notification) => { - await dispatch('fetchOne', notification) - // Fetch total after notification is added - // Because it triggers infinite loader - dispatch('fetchTotal') - }) - .on('patched', (notification) => { - dispatch('fetchOne', notification) + mutations: { + total (state, total) { + state.total = total + }, + unseenTotal (state, unseenTotal) { + state.unseenTotal = unseenTotal + }, + set (state, notifications) { + if (!notifications || notifications === undefined) { + state.notifications = null + } else { + state.notifications = notifications + } + }, + setOnlyUnseen (state, val) { + state.onlyUnseen = val + }, + clear (state) { + state.total = 0 + state.unseenTotal = 0 + state.notifications = [] + }, + add (state, notification) { + let toBottom = false + if (notification.toBottom) { + // to an object with more options, unwrap it... + toBottom = true + notification = notification.notification + } + const index = state.notifications.findIndex(item => { + return item._id === notification._id }) - }, - find ({state, commit, rootGetters}, queryParams) { - let query = { - $limit: options.limit, - $sort: { - createdAt: -1 - }, - userId: rootGetters['auth/user']._id, - ...queryParams - } - if (state.onlyUnseen) { - query.unseen = true + if (index > -1) { + // Replace existing notification + state.notifications.splice(index, 1, notification) + } else if (toBottom) { + // Or add new one + state.notifications.push(notification) + } else { + // Or add new one + state.notifications.unshift(notification) + } + }, + isLoading (state, isLoading) { + state.isLoading = isLoading } - commit('isLoading', true) - return this.app.$api.service('notifications').find({ query }) }, - fetch ({commit, dispatch}) { - dispatch('fetchTotal') - return dispatch('find') - .then(result => { - commit('isLoading', false) - commit('set', result.data) - }) - .catch(error => { - commit('isLoading', false) - console.error('fetch could not fetch notifications', error) - }) - }, - fetchMore ({state, commit, dispatch}) { - const $skip = state.notifications.length - return dispatch('find', { $skip }) - .then(result => { - commit('isLoading', false) - dispatch('addMany', result.data) - }) - .catch(error => { - commit('isLoading', false) - console.error('fetchMore could not fetch notifications', error) - }) - }, - fetchOne ({commit, dispatch, rootGetters}, notification) { - // Only fetch notification for current user - if (notification.userId !== rootGetters['auth/user']._id) { - return + + getters: { + all (state) { + return state.notifications + }, + total (state) { + return state.total + }, + unseenTotal (state) { + return state.unseenTotal + }, + onlyUnseen (state) { + return state.onlyUnseen + }, + hasMore (state) { + const total = state.onlyUnseen ? state.unseenTotal : state.total + return total && + total > state.notifications.length + }, + isLoading (state) { + return state.isLoading } - commit('isLoading', true) - return this.app.$api.service('notifications').get(notification._id) - .then((result) => { - commit('isLoading', false) - commit('add', result) - }) - .catch(error => { - commit('isLoading', false) - console.error('fetchOne could not fetch notification:', notification._id) - console.error(error) - }) }, - fetchTotal ({commit, rootGetters}) { - let requests = [] - requests.push(this.app.$api.service('notifications').find({ - query: { - $limit: 0, + + actions: { + // Called from nuxtServerInit in index + init ({dispatch}) { + return dispatch('fetch') + }, + // Called from plugins/init-store-subscriptions only once + subscribe ({dispatch}) { + return this.app.$api.service('notifications') + .on('created', async (notification) => { + await dispatch('fetchOne', notification) + // Fetch total after notification is added + // Because it triggers infinite loader + dispatch('fetchTotal') + }) + .on('patched', (notification) => { + dispatch('fetchOne', notification) + }) + }, + find ({state, commit, rootGetters}, queryParams) { + let query = { + $limit: options.limit, + $sort: { + createdAt: -1 + }, userId: rootGetters['auth/user']._id, - unseen: true + ...queryParams } - }) - .then((result) => { - commit('unseenTotal', result.total) - }) - .catch(() => { - commit('clear') - })) - requests.push(this.app.$api.service('notifications').find({ - query: { - $limit: 0, - userId: rootGetters['auth/user']._id + if (state.onlyUnseen) { + query.unseen = true } - }) - .then((result) => { - commit('total', result.total) - }) - .catch(() => { - commit('clear') - })) - return Promise.all(requests) - }, - toggleUnseen ({state, commit, dispatch}) { - commit('setOnlyUnseen', !state.onlyUnseen) - dispatch('fetch') - }, - addMany ({state, commit}, notifications) { - notifications = _.castArray(notifications) - if (notifications || notifications.length) { - notifications.forEach(notification => { - commit('add', { notification, toBottom: true }) - }) - } - }, - async markAsRead ({dispatch}, {notification}) { - let result - - if (notification) { - // mark all as read with the same contribution id - let query = [{ - id: notification._id - }] - - if (!_.isEmpty(notification.relatedContributionId)) { - query.push({ - relatedContributionId: notification.relatedContributionId, - unseen: true + commit('isLoading', true) + return this.app.$api.service('notifications').find({ query }) + }, + fetch ({commit, dispatch}) { + dispatch('fetchTotal') + return dispatch('find') + .then(result => { + commit('isLoading', false) + commit('set', result.data) }) + .catch(error => { + commit('isLoading', false) + console.error('fetch could not fetch notifications', error) + }) + }, + fetchMore ({state, commit, dispatch}) { + const $skip = state.notifications.length + return dispatch('find', { $skip }) + .then(result => { + commit('isLoading', false) + dispatch('addMany', result.data) + }) + .catch(error => { + commit('isLoading', false) + console.error('fetchMore could not fetch notifications', error) + }) + }, + fetchOne ({commit, dispatch, rootGetters}, notification) { + // Only fetch notification for current user + if (notification.userId !== rootGetters['auth/user']._id) { + return } - - result = await this.app.$api.service('notifications').patch(null, { - unseen: false - }, { + commit('isLoading', true) + return this.app.$api.service('notifications').get(notification._id) + .then((result) => { + commit('isLoading', false) + commit('add', result) + }) + .catch(error => { + commit('isLoading', false) + console.error('fetchOne could not fetch notification:', notification._id) + console.error(error) + }) + }, + fetchTotal ({commit, rootGetters}) { + let requests = [] + requests.push(this.app.$api.service('notifications').find({ query: { - $or: query + $limit: 0, + userId: rootGetters['auth/user']._id, + unseen: true } }) - } else { - // mark all as read - result = await this.app.$api.service('notifications').patch(null, { - unseen: false - }, { + .then((result) => { + commit('unseenTotal', result.total) + }) + .catch(() => { + commit('clear') + })) + requests.push(this.app.$api.service('notifications').find({ query: { - unseen: true + $limit: 0, + userId: rootGetters['auth/user']._id } }) - } + .then((result) => { + commit('total', result.total) + }) + .catch(() => { + commit('clear') + })) + return Promise.all(requests) + }, + toggleUnseen ({state, commit, dispatch}) { + commit('setOnlyUnseen', !state.onlyUnseen) + dispatch('fetch') + }, + addMany ({state, commit}, notifications) { + notifications = _.castArray(notifications) + if (notifications || notifications.length) { + notifications.forEach(notification => { + commit('add', { notification, toBottom: true }) + }) + } + }, + async markAsRead ({dispatch}, {notification}) { + let result - dispatch('fetchTotal') - return result + if (notification) { + // mark all as read with the same contribution id + let query = [{ + id: notification._id + }] + + if (!_.isEmpty(notification.relatedContributionId)) { + query.push({ + relatedContributionId: notification.relatedContributionId, + unseen: true + }) + } + + result = await this.app.$api.service('notifications').patch(null, { + unseen: false + }, { + query: { + $or: query + } + }) + } else { + // mark all as read + result = await this.app.$api.service('notifications').patch(null, { + unseen: false + }, { + query: { + unseen: true + } + }) + } + + dispatch('fetchTotal') + return result + } } } diff --git a/store/organizations.js b/store/organizations.js index e0464f94..f9b752d2 100644 --- a/store/organizations.js +++ b/store/organizations.js @@ -1,18 +1,21 @@ -export const actions = { - async patch ({dispatch}, organization) { - if (!organization) { - return null +export default { + namespaced: true, + actions: { + async patch ({dispatch}, organization) { + if (!organization) { + return null + } + return this.app.$api.service('organizations').patch(organization._id, organization) + }, + create ({dispatch}, organization) { + return this.app.$api.service('organizations').create(organization) + }, + follow ({dispatch}, data) { + return this.app.$api.service('follows').create({ + followingId: data.organizationId, + type: 'organizations', + userId: data.currentUserId + }) } - return this.app.$api.service('organizations').patch(organization._id, organization) - }, - create ({dispatch}, organization) { - return this.app.$api.service('organizations').create(organization) - }, - follow ({dispatch}, data) { - return this.app.$api.service('follows').create({ - followingId: data.organizationId, - type: 'organizations', - userId: data.currentUserId - }) } } diff --git a/store/search.js b/store/search.js index 6de8c7e3..b78b4ca5 100644 --- a/store/search.js +++ b/store/search.js @@ -1,77 +1,77 @@ import { clone } from 'lodash' import searchQueryBuilder from './utils/search-query-builder' -export const state = () => { - return { +export default { + namespaced: true, + state: { query: '', filter: { categoryIds: [], emotions: [] } - } -} + }, -export const mutations = { - query (state, query) { - if (!query || query === undefined) { - state.query = null - } else { - state.query = query + mutations: { + query (state, query) { + if (!query || query === undefined) { + state.query = null + } else { + state.query = query - // go to newsfeed to see search results - if (this.app.router.currentRoute.name !== 'index') { - this.app.router.push({ name: 'index' }) + // go to newsfeed to see search results + if (this.app.router.currentRoute.name !== 'index') { + this.app.router.push({ name: 'index' }) + } + } + }, + categoryIds (state, categoryIds) { + if (!categoryIds || categoryIds === undefined) { + state.filter.categoryIds = [] + } else { + state.filter.categoryIds = clone(categoryIds) + } + }, + emotions (state, emotions) { + if (!emotions || emotions === undefined) { + state.filter.emotions = [] + } else { + state.filter.emotions = clone(emotions) } } }, - categoryIds (state, categoryIds) { - if (!categoryIds || categoryIds === undefined) { - state.filter.categoryIds = [] - } else { - state.filter.categoryIds = clone(categoryIds) - } - }, - emotions (state, emotions) { - if (!emotions || emotions === undefined) { - state.filter.emotions = [] - } else { - state.filter.emotions = clone(emotions) - } - } -} -export const getters = { - query (state) { - return state.query - }, - categoryIds (state) { - return clone(state.filter.categoryIds) - }, - emotions (state) { - return clone(state.filter.emotions) - }, - all (state) { - return state - }, - queryEmotions (state, getters, rootState, rootGetters) { - // generate the emotions filter query by using the selected emotions - return searchQueryBuilder.buildFilterEmotions(getters.emotions, {}) - }, - queryCategories (state, getters, rootState, rootGetters) { - // generate the category filter query by using the selected category ids - return searchQueryBuilder.buildFilterCategories(getters.categoryIds, {}) - }, - queryLanguages (state, getters, rootState, rootGetters) { - if (rootState.auth.user) { - const languages = rootGetters['auth/userSettings'].contentLanguages - return searchQueryBuilder.buildFilterLanguages(languages, {}) - } else { - return {} + getters: { + query (state) { + return state.query + }, + categoryIds (state) { + return clone(state.filter.categoryIds) + }, + emotions (state) { + return clone(state.filter.emotions) + }, + all (state) { + return state + }, + queryEmotions (state, getters, rootState, rootGetters) { + // generate the emotions filter query by using the selected emotions + return searchQueryBuilder.buildFilterEmotions(getters.emotions, {}) + }, + queryCategories (state, getters, rootState, rootGetters) { + // generate the category filter query by using the selected category ids + return searchQueryBuilder.buildFilterCategories(getters.categoryIds, {}) + }, + queryLanguages (state, getters, rootState, rootGetters) { + if (rootState.auth.user) { + const languages = rootGetters['auth/userSettings'].contentLanguages + return searchQueryBuilder.buildFilterLanguages(languages, {}) + } else { + return {} + } + }, + querySearch (state, getters, rootState, rootGetters) { + } }, - querySearch (state, getters, rootState, rootGetters) { - - } + actions: {} } - -export const actions = {} diff --git a/store/services/users.js b/store/services/users.js new file mode 100644 index 00000000..ae200954 --- /dev/null +++ b/store/services/users.js @@ -0,0 +1,11 @@ +import feathersVuex from 'feathers-vuex' + +let servicePlugin = (feathersClient) => { + const { service } = feathersVuex(feathersClient, { idField: '_id' }) + const servicePath = 'users' + const servicePlugin = service(servicePath, { + namespace: 'feathers-vuex-users' + }) + return servicePlugin +} +export default servicePlugin diff --git a/store/services/usersettings.js b/store/services/usersettings.js new file mode 100644 index 00000000..233538fc --- /dev/null +++ b/store/services/usersettings.js @@ -0,0 +1,11 @@ +import feathersVuex from 'feathers-vuex' + +let servicePlugin = (feathersClient) => { + const { service } = feathersVuex(feathersClient, { idField: '_id' }) + const servicePath = 'usersettings' + const servicePlugin = service(servicePath, { + namespace: 'feathers-vuex-usersettings' + }) + return servicePlugin +} +export default servicePlugin diff --git a/store/settings.js b/store/settings.js index 1d859e8f..1a0ef23e 100644 --- a/store/settings.js +++ b/store/settings.js @@ -1,7 +1,8 @@ import {isArray, intersection} from 'lodash' -export const state = () => { - return { +export default { + namespaced: true, + state: { settings: { _id: null, invites: { @@ -11,79 +12,78 @@ export const state = () => { }, maintenance: false } - } -} - -export const mutations = { - set (state, settings) { - state.settings = Object.assign(state.settings, settings) - } -} - -export const getters = { - get (state) { - return state.settings }, - showInvites (state, getters, rootState, rootGetters) { - if (!state.settings.invites.userCanInvite) { - return false + + mutations: { + set (state, settings) { + state.settings = Object.assign(state.settings, settings) } + }, - const user = rootGetters['auth/user'] - const badgeIds = user.badgeIds || [] - const inviteBadgeIds = state.settings.invites.onlyUserWithBadgesCanInvite + getters: { + get (state) { + return state.settings + }, + showInvites (state, getters, rootState, rootGetters) { + if (!state.settings.invites.userCanInvite) { + return false + } - if (user.role === 'admin') { - return true - } + const user = rootGetters['auth/user'] + const badgeIds = user.badgeIds || [] + const inviteBadgeIds = state.settings.invites.onlyUserWithBadgesCanInvite - if (inviteBadgeIds.length) { - return intersection(badgeIds, inviteBadgeIds).length - } - return state.settings.invites.userCanInvite - } -} + if (user.role === 'admin') { + return true + } -export const actions = { - // Called from nuxtServerInit in index - init ({dispatch}) { - return dispatch('fetch') - }, - // Called from plugins/init-store-subscriptions only once - subscribe ({state, commit}) { - return this.app.$api.service('settings') - .on('patched', (data) => { - if (state.settings._id === data._id) { - commit('set', data) - } - }) - }, - async fetch ({commit}) { - const service = this.app.$api.service('settings') - let res = await service.find({query: {key: 'system'}}) - if (isArray(res)) { - res = res.pop() + if (inviteBadgeIds.length) { + return intersection(badgeIds, inviteBadgeIds).length + } + return state.settings.invites.userCanInvite } + }, + actions: { + // Called from nuxtServerInit in index + init ({dispatch}) { + return dispatch('fetch') + }, + // Called from plugins/init-store-subscriptions only once + subscribe ({state, commit}) { + return this.app.$api.service('settings') + .on('patched', (data) => { + if (state.settings._id === data._id) { + commit('set', data) + } + }) + }, + async fetch ({commit}) { + const service = this.app.$api.service('settings') + let res = await service.find({query: {key: 'system'}}) + if (isArray(res)) { + res = res.pop() + } - await commit('set', res) + await commit('set', res) - return res - }, - async patch ({state, commit}, data) { - data = JSON.parse(JSON.stringify(data)) - const service = this.app.$api.service('settings') - let res - if (state.settings._id) { - res = await service.patch(state.settings._id, Object.assign(JSON.parse(JSON.stringify(state.settings)), data)) - } else { - res = await service.create(data) - } - if (isArray(res)) { - res = res.pop() - } + return res + }, + async patch ({state, commit}, data) { + data = JSON.parse(JSON.stringify(data)) + const service = this.app.$api.service('settings') + let res + if (state.settings._id) { + res = await service.patch(state.settings._id, Object.assign(JSON.parse(JSON.stringify(state.settings)), data)) + } else { + res = await service.create(data) + } + if (isArray(res)) { + res = res.pop() + } - await commit('set', res) + await commit('set', res) - return res + return res + } } } diff --git a/store/usersettings.js b/store/usersettings.js index 1744a5a9..62b635fb 100644 --- a/store/usersettings.js +++ b/store/usersettings.js @@ -1,22 +1,25 @@ import { isArray } from 'lodash' -export const actions = { - async patch ({dispatch, rootGetters}, data) { - const user = rootGetters['auth/user'] - const userSettings = rootGetters['auth/userSettings'] +export default { + namespaced: true, + actions: { + async patch ({dispatch, rootGetters}, data) { + const user = rootGetters['auth/user'] + const userSettings = rootGetters['auth/userSettings'] - if (!user || !userSettings) { - return null - } - data.userId = user._id.toString() + if (!user || !userSettings) { + return null + } + data.userId = user._id.toString() - let res = await this.app.$api.service('usersettings').create(data) - if (isArray(res)) { - res = res.pop() - } + let res = await this.app.$api.service('usersettings').create(data) + if (isArray(res)) { + res = res.pop() + } - await dispatch('auth/refreshUser', res, { root: true }) + await dispatch('auth/refreshUser', res, { root: true }) - return res + return res + } } } diff --git a/yarn.lock b/yarn.lock index 3ebc83a2..ee71a04d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -250,9 +250,9 @@ version "3.0.0" resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.0.0.tgz#c1de4293081424da3ac30c23afa850af1019bb54" -"@nuxtjs/dotenv@~1.1.0": +"@nuxtjs/dotenv@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@nuxtjs/dotenv/-/dotenv-1.1.1.tgz#c3d68f96d8f76cac4c4ddbc8268702eb95737bbe" + resolved "http://registry.npmjs.org/@nuxtjs/dotenv/-/dotenv-1.1.1.tgz#c3d68f96d8f76cac4c4ddbc8268702eb95737bbe" dependencies: dotenv "^5.0.0" @@ -1648,7 +1648,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@^3.0.0, bluebird@^3.1.1, bluebird@^3.4.7, bluebird@^3.5.0, bluebird@^3.5.1: +bluebird@^3.0.0, bluebird@^3.1.1, bluebird@^3.4.7, bluebird@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1780,7 +1780,7 @@ brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" -browser-env@~3.2.4: +browser-env@^3.2.5: version "3.2.5" resolved "https://registry.yarnpkg.com/browser-env/-/browser-env-3.2.5.tgz#4345b8094413552e1e32c0c7b048b85d90965cc1" dependencies: @@ -2653,6 +2653,12 @@ cookie-universal@^2.0.3: dependencies: cookie "^0.3.1" +cookie-universal@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cookie-universal/-/cookie-universal-2.0.5.tgz#42f109b96f9ff7e496e79914aaf77a4978ecab7e" + dependencies: + cookie "^0.3.1" + cookie@0.3.1, cookie@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" @@ -2990,14 +2996,6 @@ dasherize@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308" -data-urls@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.0.0.tgz#24802de4e81c298ea8a9388bb0d8e461c774684f" - dependencies: - abab "^1.0.4" - whatwg-mimetype "^2.0.0" - whatwg-url "^6.4.0" - date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -3022,7 +3020,7 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6. dependencies: ms "2.0.0" -debug@^3.0.1, debug@^3.1.0, debug@~3.1.0: +debug@^3.0.0, debug@^3.0.1, debug@^3.1.0, debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -3167,10 +3165,6 @@ detect-port-alt@1.1.6: address "^1.0.1" debug "^2.6.0" -diff@^3.2.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3454,17 +3448,6 @@ envify@^4.0.0: esprima "^4.0.0" through "~2.3.4" -envsub@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/envsub/-/envsub-3.1.0.tgz#06a9ef032d026771018335ff8d15e53bdd10cc22" - dependencies: - bluebird "^3.5.0" - chalk "^1.1.3" - commander "^2.9.0" - diff "^3.2.0" - handlebars "^4.0.6" - lodash "^4.17.4" - equal-length@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/equal-length/-/equal-length-1.0.1.tgz#21ca112d48ab24b4e1e7ffc0e5339d31fdfc274c" @@ -4058,6 +4041,10 @@ falafel@^2.1.0: isarray "0.0.1" object-keys "^1.0.6" +fast-copy@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-1.2.2.tgz#b15cf14d767f07c22cd5ffbd6cc615c12d206b7e" + fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -4106,6 +4093,39 @@ fbjs@^0.8.16: setimmediate "^1.0.5" ua-parser-js "^0.7.9" +feathers-commons@^0.8.0: + version "0.8.7" + resolved "https://registry.yarnpkg.com/feathers-commons/-/feathers-commons-0.8.7.tgz#11c6f25b537745a983e8d61552d7db8932d53782" + +feathers-errors@^2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/feathers-errors/-/feathers-errors-2.9.2.tgz#96ca0e5fe50cc56f0eccc90ce3fa5e1f8840828d" + dependencies: + debug "^3.0.0" + +feathers-query-filters@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/feathers-query-filters/-/feathers-query-filters-2.1.2.tgz#cdb18224db5e19cc0140d528108e0908d5eb0654" + dependencies: + feathers-commons "^0.8.0" + +feathers-vuex@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/feathers-vuex/-/feathers-vuex-1.4.8.tgz#c3ada49b72cdcf72d4e19b9f63435584a216136d" + dependencies: + "@feathersjs/commons" "^1.4.1" + debug "^3.1.0" + fast-copy "^1.2.0" + feathers-errors "^2.9.2" + feathers-query-filters "^2.1.2" + inflection "^1.12.0" + jwt-decode "^2.2.0" + lodash.isobject "^3.0.2" + lodash.merge "^4.6.0" + lodash.trim "^4.5.1" + serialize-error "^2.1.0" + sift "^5.0.0" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -4702,7 +4722,7 @@ handle-thing@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" -handlebars@^4.0.1, handlebars@^4.0.6: +handlebars@^4.0.1: version "4.0.11" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" dependencies: @@ -5181,6 +5201,10 @@ indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" +inflection@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -5838,37 +5862,6 @@ jsdom@11.6.2: ws "^4.0.0" xml-name-validator "^3.0.0" -jsdom@~11.10.0: - version "11.10.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.10.0.tgz#a42cd54e88895dc765f03f15b807a474962ac3b5" - dependencies: - abab "^1.0.4" - acorn "^5.3.0" - acorn-globals "^4.1.0" - array-equal "^1.0.0" - cssom ">= 0.3.2 < 0.4.0" - cssstyle ">= 0.2.37 < 0.3.0" - data-urls "^1.0.0" - domexception "^1.0.0" - escodegen "^1.9.0" - html-encoding-sniffer "^1.0.2" - left-pad "^1.2.0" - nwmatcher "^1.4.3" - parse5 "4.0.0" - pn "^1.1.0" - request "^2.83.0" - request-promise-native "^1.0.5" - sax "^1.2.4" - symbol-tree "^3.2.2" - tough-cookie "^2.3.3" - w3c-hr-time "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.3" - whatwg-mimetype "^2.1.0" - whatwg-url "^6.4.0" - ws "^4.0.0" - xml-name-validator "^3.0.0" - jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -5988,7 +5981,7 @@ jss@^9.8.1: symbol-observable "^1.1.0" warning "^3.0.0" -jwt-decode@^2.1.0: +jwt-decode@^2.1.0, jwt-decode@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" @@ -6240,6 +6233,10 @@ lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isobject@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -6285,6 +6282,10 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" +lodash.trim@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/lodash.trim/-/lodash.trim-4.5.1.tgz#36425e7ee90be4aa5e27bcebb85b7d11ea47aa57" + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -7022,6 +7023,10 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" +nuxt-env@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/nuxt-env/-/nuxt-env-0.0.3.tgz#fcd1a614c95fa0ed61adeac37d1d5e1107a57b78" + nuxt@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/nuxt/-/nuxt-1.4.0.tgz#66df9ede68daf2ef4cfbcc329da7dda0003e0420" @@ -9433,6 +9438,10 @@ shuffle-seed@^1.1.6: dependencies: seedrandom "^2.4.2" +sift@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/sift/-/sift-5.1.0.tgz#1bbf2dfb0eb71e56c4cc7fb567fbd1351b65015e" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -11020,6 +11029,12 @@ vue-template-es2015-compiler@^1.5.0, vue-template-es2015-compiler@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18" +vue-test-utils@^1.0.0-beta.11: + version "1.0.0-beta.11" + resolved "https://registry.yarnpkg.com/vue-test-utils/-/vue-test-utils-1.0.0-beta.11.tgz#41b7fa53e0061d16ecbe18cfeea197c909d4ab8a" + dependencies: + lodash "^4.17.4" + vue-text-glitch@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/vue-text-glitch/-/vue-text-glitch-1.0.1.tgz#bc2d7bfd6413bed7597718d5b6ed3ae1a5374f7f" @@ -11275,10 +11290,6 @@ whatwg-fetch@>=0.10.0: version "2.0.4" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" -whatwg-mimetype@^2.0.0, whatwg-mimetype@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f0f21d76cbba72362eb609dbed2a30cd17fcc7d4" - whatwg-url@^6.4.0: version "6.4.1" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.1.tgz#fdb94b440fd4ad836202c16e9737d511f012fd67"