mirror of
https://framagit.org/les/gancio.git
synced 2025-01-31 08:32:23 +01:00
start with nuxt
This commit is contained in:
parent
afa6ad2a6b
commit
88b43f9bb1
54 changed files with 2742 additions and 0 deletions
13
.editorconfig
Normal file
13
.editorconfig
Normal file
|
@ -0,0 +1,13 @@
|
|||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
84
.gitignore
vendored
Normal file
84
.gitignore
vendored
Normal file
|
@ -0,0 +1,84 @@
|
|||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
7
assets/README.md
Normal file
7
assets/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# ASSETS
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
|
94
components/Calendar.vue
Normal file
94
components/Calendar.vue
Normal file
|
@ -0,0 +1,94 @@
|
|||
<template lang="pug">
|
||||
v-calendar#calendar.card(
|
||||
show-caps
|
||||
:popover-expanded='true'
|
||||
:attributes='attributes'
|
||||
:from-page.sync='page'
|
||||
is-expanded is-inline)
|
||||
div(slot='popover', slot-scope='{ customData }')
|
||||
router-link(:to="`/event/${customData.id}`") {{customData.start_datetime|hour}} - {{customData.title}} @{{customData.place.name}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
// import filters from '@/filters'
|
||||
import moment from 'dayjs'
|
||||
import { intersection } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'Calendar',
|
||||
// filters,
|
||||
data () {
|
||||
const month = moment().month()+1
|
||||
const year = moment().year()
|
||||
return {
|
||||
page: { month, year},
|
||||
}
|
||||
},
|
||||
|
||||
// async mounted () {
|
||||
// await this.updateEvents(this.page)
|
||||
// },
|
||||
watch: {
|
||||
page () {
|
||||
this.updateEvents(this.page)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateEvents']),
|
||||
eventToAttribute(event) {
|
||||
let e = {
|
||||
key: event.id,
|
||||
customData: event,
|
||||
order: event.start_datetime,
|
||||
popover: {
|
||||
slot: 'popover',
|
||||
visibility: 'hover'
|
||||
}
|
||||
}
|
||||
|
||||
let color = event.tags && event.tags.length && event.tags[0].color ? event.tags[0].color : 'rgba(170,170,250,0.7)'
|
||||
if (event.past) color = 'rgba(200,200,200,0.5)'
|
||||
if (event.multidate) {
|
||||
e.dates = {
|
||||
start: event.start_datetime, end: event.end_datetime
|
||||
}
|
||||
e.highlight = { backgroundColor: color,
|
||||
borderColor: 'transparent',
|
||||
borderWidth: '4px' }
|
||||
} else {
|
||||
e.dates = event.start_datetime
|
||||
e.dot = { backgroundColor: color, borderColor: color, borderWidth: '3px' }
|
||||
}
|
||||
return e
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredEvents () {
|
||||
return this.$store.getters.filteredEvents
|
||||
},
|
||||
...mapState(['events', 'filters', 'user', 'logged']),
|
||||
attributes () {
|
||||
return [
|
||||
{ key: 'todaly', dates: new Date(),
|
||||
highlight: {
|
||||
backgroundColor: '#aaffaa'
|
||||
},
|
||||
popover: {label: this.$t('Today')}
|
||||
},
|
||||
...this.filteredEvents.map(this.eventToAttribute)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#calendar {
|
||||
margin-bottom: 0em;
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
#calendar a {
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
64
components/Event.vue
Normal file
64
components/Event.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template lang="pug">
|
||||
b-card(bg-variant='dark' text-variant='white' :class="{ withImg: event.image_path ? true : false }"
|
||||
@click='$router.push("/event/" + event.id)'
|
||||
:img-src='imgPath')
|
||||
strong {{event.title}}
|
||||
div <v-icon name='clock'/> {{event.start_datetime|datetime}}
|
||||
//- span <v-icon name='map-marker-alt'/> {{event.place.name}}
|
||||
br
|
||||
el-tag.mr-1(:color='tag.color || "grey"' v-for='tag in event.tags' :key='tag.tag'
|
||||
size='small' @click.stop='addSearchTag(tag)') {{tag.tag}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
// import api from '@/server/api'
|
||||
// import filters from '@/filters'
|
||||
|
||||
export default {
|
||||
props: ['event'],
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
imgPath() {
|
||||
return this.event.image_path && '/uploads/thumb/' + this.event.image_path
|
||||
},
|
||||
mine() {
|
||||
return this.event.userId === this.user.id
|
||||
}
|
||||
},
|
||||
// filters: {
|
||||
// datetime: this.datetime
|
||||
// },
|
||||
methods: {
|
||||
...mapActions(['delEvent', 'addSearchTag']),
|
||||
async remove() {
|
||||
// await api.delEvent(this.event.id)
|
||||
// this.delEvent(this.event.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
/* .card::before {
|
||||
border-top: 4px solid black;
|
||||
content: ''
|
||||
} */
|
||||
|
||||
.el-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-card img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-columns .card {
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
.card-img {
|
||||
height: 180px;
|
||||
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
100
components/Home.vue
Normal file
100
components/Home.vue
Normal file
|
@ -0,0 +1,100 @@
|
|||
<template lang="pug">
|
||||
div
|
||||
magic-grid(:animate="false" useMin :gap=5 :maxCols=4
|
||||
:maxColWidth='400' ref='magicgrid')
|
||||
div.mt-1.item
|
||||
//- Search#search
|
||||
no-ssr
|
||||
Calendar
|
||||
Event.item.mt-1(v-for='event in events'
|
||||
:key='event.id'
|
||||
:event='event')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import axios from 'axios'
|
||||
// import filters from '@/filters.js'
|
||||
import Event from '@/components/Event'
|
||||
import Calendar from '@/components/Calendar'
|
||||
// import { intersection } from 'lodash'
|
||||
// import moment from 'dayjs'
|
||||
// import Search from '@/components/Search'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
// async asyncData ({req}) {
|
||||
// console.log('dentro asyncData')
|
||||
// const { data } = await axios.get('http://localhost:3000/api/event/2019/2')
|
||||
// return { events: data }
|
||||
// },
|
||||
components: { Event, Calendar }, // , Calendar, Search },
|
||||
computed: {
|
||||
...mapState(['events', 'filters']),
|
||||
filteredEvents() {
|
||||
return this.events
|
||||
// return this.$store.getters.filteredEvents
|
||||
.filter(e => !e.past)
|
||||
.sort((a, b) => a.start_datetime > b.start_datetime)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filteredEvents() {
|
||||
// this.$nextTick(this.$refs.magicgrid.positionItems) TOFIX
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#search {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin-top: 4px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-columns {
|
||||
column-count: 1;
|
||||
column-gap: 0.2em;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.container {
|
||||
max-width: none;
|
||||
}
|
||||
.card-columns {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 950px) {
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
}
|
||||
.card-columns {
|
||||
column-count: 3;
|
||||
}
|
||||
}
|
||||
|
||||
/* .item {
|
||||
transition: all .2s;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
} */
|
||||
/* .list-enter, .list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 0px;
|
||||
left: 0px;
|
||||
height: 0px;
|
||||
z-index: -10;
|
||||
} */
|
||||
</style>
|
79
components/Logo.vue
Normal file
79
components/Logo.vue
Normal file
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div class="VueToNuxtLogo">
|
||||
<div class="Triangle Triangle--two" />
|
||||
<div class="Triangle Triangle--one" />
|
||||
<div class="Triangle Triangle--three" />
|
||||
<div class="Triangle Triangle--four" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.VueToNuxtLogo {
|
||||
display: inline-block;
|
||||
animation: turn 2s linear forwards 1s;
|
||||
transform: rotateX(180deg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 180px;
|
||||
width: 245px;
|
||||
}
|
||||
|
||||
.Triangle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.Triangle--one {
|
||||
border-left: 105px solid transparent;
|
||||
border-right: 105px solid transparent;
|
||||
border-bottom: 180px solid #41b883;
|
||||
}
|
||||
|
||||
.Triangle--two {
|
||||
top: 30px;
|
||||
left: 35px;
|
||||
animation: goright 0.5s linear forwards 3.5s;
|
||||
border-left: 87.5px solid transparent;
|
||||
border-right: 87.5px solid transparent;
|
||||
border-bottom: 150px solid #3b8070;
|
||||
}
|
||||
|
||||
.Triangle--three {
|
||||
top: 60px;
|
||||
left: 35px;
|
||||
animation: goright 0.5s linear forwards 3.5s;
|
||||
border-left: 70px solid transparent;
|
||||
border-right: 70px solid transparent;
|
||||
border-bottom: 120px solid #35495e;
|
||||
}
|
||||
|
||||
.Triangle--four {
|
||||
top: 120px;
|
||||
left: 70px;
|
||||
animation: godown 0.5s linear forwards 3s;
|
||||
border-left: 35px solid transparent;
|
||||
border-right: 35px solid transparent;
|
||||
border-bottom: 60px solid #fff;
|
||||
}
|
||||
|
||||
@keyframes turn {
|
||||
100% {
|
||||
transform: rotateX(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes godown {
|
||||
100% {
|
||||
top: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes goright {
|
||||
100% {
|
||||
left: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
50
components/Nav.vue
Normal file
50
components/Nav.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template lang="pug">
|
||||
b-navbar(type="dark" variant="dark" toggleable='md')
|
||||
b-navbar-toggle(target='nav_collapse')
|
||||
b-navbar-brand(to='/') <img id='logo' src='gancio_logo.svg'/>
|
||||
b-collapse#nav_collapse(is-nav)
|
||||
b-navbar-nav
|
||||
b-nav-item(v-if='!logged' to='/login' v-b-tooltip :title='$t("Login")') <v-icon color='lightgreen' name='lock' />
|
||||
span.d-md-none {{$t('User')}}
|
||||
b-nav-item(to='/new_event' v-b-tooltip :title='$t("Add Event")' ) <v-icon color='lightgreen' name='plus'/>
|
||||
span.d-md-none {{$t('Add Event')}}
|
||||
b-nav-item(v-if='logged' to='/settings' v-b-tooltip :title='$t("Settings")') <v-icon color='orange' name='cog'/>
|
||||
span.d-md-none {{$t('Settings')}}
|
||||
b-nav-item(v-if='user.is_admin' to='/admin' v-b-tooltip :title='$t("Admin")') <v-icon color='lightblue' name='tools'/>
|
||||
span.d-md-none {{$t('Admin')}}
|
||||
b-nav-item(to='/export' v-b-tooltip :title='$t("Export")') <v-icon name='file-export' color='yellow'/>
|
||||
span.d-md-none {{$t('Export')}}
|
||||
b-nav-item(v-if='logged' @click='logout' v-b-tooltip :title='$t("Logout")') <v-icon color='red' name='sign-out-alt'/>
|
||||
span.d-md-none {{$t('Logout')}}
|
||||
b-navbar-nav.ml-auto
|
||||
b-nav-item(to='/about')
|
||||
span {{$t('Info')}} <v-icon color='#ff9fc4' name='question-circle'/>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import {mapState, mapActions} from 'vuex'
|
||||
export default {
|
||||
name: 'Nav',
|
||||
computed: {
|
||||
...mapState(['logged', 'user','filters']),
|
||||
filters_tags: {
|
||||
set (value) {
|
||||
this.setSearchTags(value)
|
||||
},
|
||||
get () {
|
||||
return this.filters.tags
|
||||
}
|
||||
},
|
||||
filters_places: {
|
||||
set (value) {
|
||||
this.setSearchPlaces(value)
|
||||
},
|
||||
get () {
|
||||
return this.filters.places
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: mapActions(['logout']),
|
||||
}
|
||||
</script>
|
||||
|
7
components/README.md
Normal file
7
components/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# COMPONENTS
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
The components directory contains your Vue.js Components.
|
||||
|
||||
_Nuxt.js doesn't supercharge these components._
|
39
components/Search.vue
Normal file
39
components/Search.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template lang="pug">
|
||||
div
|
||||
el-select.mr-1(v-model='filters_places' multiple filterable collapse-tags
|
||||
default-first-option :placeholder='$t("Where")')
|
||||
el-option(v-for='place in places' :value='place.name'
|
||||
:label='place.name' :key='place.id')
|
||||
el-select(v-model='filters_tags' multiple filterable collapse-tags
|
||||
default-first-option :placeholder='$t("Tags")')
|
||||
el-option(v-for='tag in tags' :key='tag.tag'
|
||||
:label='tag.tag' :value='tag.tag')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState, mapActions} from 'vuex'
|
||||
|
||||
export default {
|
||||
name :'Search',
|
||||
methods: mapActions(['setSearchPlaces', 'setSearchTags']),
|
||||
computed: {
|
||||
...mapState(['tags', 'places', 'filters']),
|
||||
filters_tags: {
|
||||
set (value) {
|
||||
this.setSearchTags(value)
|
||||
},
|
||||
get () {
|
||||
return this.filters.tags
|
||||
}
|
||||
},
|
||||
filters_places: {
|
||||
set (value) {
|
||||
this.setSearchPlaces(value)
|
||||
},
|
||||
get () {
|
||||
return this.filters.places
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
BIN
db.sqlite
Normal file
BIN
db.sqlite
Normal file
Binary file not shown.
7
layouts/README.md
Normal file
7
layouts/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# LAYOUTS
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains your Application Layouts.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts).
|
85
layouts/default.vue
Normal file
85
layouts/default.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<template lang='pug'>
|
||||
#app
|
||||
Nav
|
||||
Home
|
||||
transition(name="fade" mode="out-in")
|
||||
//- router-view(name='modal')
|
||||
nuxt
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import moment from 'dayjs'
|
||||
// import api from './api'
|
||||
// import { mapActions } from 'vuex';
|
||||
// import Register from './components/Register.vue'
|
||||
// import Login from './components/Login.vue'
|
||||
// import Settings from './components/Settings.vue'
|
||||
// import newEvent from './components/newEvent.vue'
|
||||
// import eventDetail from './components/EventDetail.vue'
|
||||
import Home from '~/components/Home.vue'
|
||||
import Nav from '~/components/Nav.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
// mounted () {
|
||||
// this.updateMeta()
|
||||
// },
|
||||
// methods: mapActions(['updateMeta']),
|
||||
// components: { Nav, Register, Login, Home, Settings, newEvent, eventDetail },
|
||||
components: { Nav, Home },
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#logo {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#search,
|
||||
#search ul {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
html, body {
|
||||
scrollbar-face-color: #313543;
|
||||
scrollbar-track-color: rgba(0, 0, 0, 0.1);
|
||||
font-family: Lato,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
|
||||
color: #2c3e50;
|
||||
background: black;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px; }
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #313543;
|
||||
border: 0px none #ffffff;
|
||||
border-radius: 6px; }
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #353a49; }
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: #313543; }
|
||||
::-webkit-scrollbar-track {
|
||||
border: 0px none #ffffff;
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.1); }
|
||||
::-webkit-scrollbar-track:hover {
|
||||
background: #282c37; }
|
||||
::-webkit-scrollbar-track:active {
|
||||
background: #282c37; }
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent; }
|
||||
|
||||
|
||||
/* .column {
|
||||
margin-top: 3px;
|
||||
margin-right: 3px;
|
||||
margin-bottom: 3px;
|
||||
width: 350px;
|
||||
} */
|
||||
|
||||
</style>
|
8
middleware/README.md
Normal file
8
middleware/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# MIDDLEWARE
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains your application middleware.
|
||||
Middleware let you define custom functions that can be run before rendering either a page or a group of pages.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
|
80
nuxt.config.js
Normal file
80
nuxt.config.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
const pkg = require('./package')
|
||||
|
||||
module.exports = {
|
||||
mode: 'universal',
|
||||
|
||||
/*
|
||||
** Headers of the page
|
||||
*/
|
||||
head: {
|
||||
title: pkg.name,
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ hid: 'description', name: 'description', content: pkg.description }
|
||||
],
|
||||
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
|
||||
},
|
||||
|
||||
serverMiddleware: [{ path: '/api', handler: '@/server/api/index.js' }],
|
||||
|
||||
/*
|
||||
** Customize the progress-bar color
|
||||
*/
|
||||
loading: { color: '#fff' },
|
||||
|
||||
/*
|
||||
** Global CSS
|
||||
*/
|
||||
css: [
|
||||
'element-ui/lib/theme-chalk/index.css',
|
||||
'bootstrap/dist/css/bootstrap.css',
|
||||
'bootstrap-vue/dist/bootstrap-vue.css',
|
||||
'v-calendar/lib/v-calendar.min.css'
|
||||
],
|
||||
|
||||
/*
|
||||
** Plugins to load before mounting the App
|
||||
*/
|
||||
plugins: ['@/plugins/element-ui', '@/plugins/filters',
|
||||
'@/plugins/i18n', '@/plugins/bootstrap-vue',
|
||||
'@/plugins/vue-awesome',
|
||||
{ src: '@/plugins/v-calendar', ssr: false },
|
||||
'@/plugins/magic-grid'],
|
||||
|
||||
/*
|
||||
** Nuxt.js modules
|
||||
*/
|
||||
modules: [
|
||||
// Doc: https://axios.nuxtjs.org/usage
|
||||
'@nuxtjs/axios'
|
||||
],
|
||||
/*
|
||||
** Axios module configuration
|
||||
*/
|
||||
axios: {
|
||||
// See https://github.com/nuxt-community/axios-module#options
|
||||
},
|
||||
|
||||
/*
|
||||
** Build configuration
|
||||
*/
|
||||
build: {
|
||||
transpile: [/^element-ui/, /^vue-awesome/, /^vue-magic-grid/],
|
||||
|
||||
/*
|
||||
** You can extend webpack config here
|
||||
*/
|
||||
extend(config, ctx) {
|
||||
// Run ESLint on save
|
||||
// if (ctx.isDev && ctx.isClient) {
|
||||
// config.module.rules.push({
|
||||
// enforce: 'pre',
|
||||
// test: /\.(js|vue)$/,
|
||||
// loader: 'eslint-loader',
|
||||
// exclude: /(node_modules)/
|
||||
// })
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
58
package.json
Normal file
58
package.json
Normal file
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "gancio",
|
||||
"version": "1.0.0",
|
||||
"description": "My well-made Nuxt.js project",
|
||||
"author": "lesion",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
|
||||
"build": "nuxt build",
|
||||
"start": "cross-env NODE_ENV=production node server/index.js",
|
||||
"generate": "nuxt generate",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
|
||||
"precommit": "npm run lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.3.6",
|
||||
"axios": "^0.18.0",
|
||||
"bcrypt": "^3.0.5",
|
||||
"bootstrap-vue": "^2.0.0-rc.16",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^5.2.0",
|
||||
"dayjs": "^1.8.11",
|
||||
"element-ui": "^2.4.11",
|
||||
"email-templates": "^5.0.4",
|
||||
"express": "^4.16.4",
|
||||
"ics": "^2.13.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mastodon-api": "^1.3.0",
|
||||
"multer": "^1.4.1",
|
||||
"nuxt": "^2.4.0",
|
||||
"sequelize": "^5.2.1",
|
||||
"sharp": "^0.22.0",
|
||||
"sqlite3": "^4.0.6",
|
||||
"v-calendar": "^0.9.7",
|
||||
"vue-awesome": "^3.5.1",
|
||||
"vue-i18n": "^8.10.0",
|
||||
"vue-magic-grid": "^0.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/eslint-config": "^0.0.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.15.1",
|
||||
"eslint-config-prettier": "^4.1.0",
|
||||
"eslint-config-standard": ">=12.0.0",
|
||||
"eslint-loader": "^2.1.2",
|
||||
"eslint-plugin-import": ">=2.16.0",
|
||||
"eslint-plugin-jest": ">=22.3.0",
|
||||
"eslint-plugin-node": ">=8.0.1",
|
||||
"eslint-plugin-nuxt": ">=0.4.2",
|
||||
"eslint-plugin-prettier": "^3.0.1",
|
||||
"eslint-plugin-promise": ">=4.0.1",
|
||||
"eslint-plugin-standard": ">=4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.2",
|
||||
"nodemon": "^1.18.9",
|
||||
"prettier": "^1.16.4",
|
||||
"pug-plain-loader": "^1.0.0"
|
||||
}
|
||||
}
|
65
pages/Login.vue
Normal file
65
pages/Login.vue
Normal file
|
@ -0,0 +1,65 @@
|
|||
<template lang='pug'>
|
||||
b-modal(@shown="$refs.email.focus()" :title='$t("Login")' hide-footer
|
||||
@hidden='$router.replace("/")' :visible='true' ref='modal')
|
||||
el-form(v-loading='loading')
|
||||
p(v-html="$t('login_explanation')")
|
||||
el-input.mb-2(v-model='email' type='email' :placeholder='$t("Email")' autocomplete='email' ref='email')
|
||||
v-icon(name='user' slot='prepend')
|
||||
el-input.mb-1(v-model='password' @keyup.enter.native="submit" type='password' :placeholder='$t("Password")')
|
||||
v-icon(name="lock" slot='prepend')
|
||||
el-button.mr-1(plain type="success" @click='submit') {{$t('Login')}}
|
||||
router-link(to='/register')
|
||||
el-button.mt-1(plain type="primary") {{$t('Not registered?')}}
|
||||
a.float-right(href='#' @click='forgot') {{$t('Forgot password?')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
import { Message } from 'element-ui'
|
||||
import api from '@/plugins/api'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
data () {
|
||||
return {
|
||||
password: '',
|
||||
email: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login']),
|
||||
async forgot () {
|
||||
if (!this.email) {
|
||||
Message({ message: this.$t('Insert your email'), type: 'error' })
|
||||
this.$refs.email.focus()
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
await api.forgotPassword(this.email)
|
||||
this.loading = false
|
||||
Message({ message: this.$t('Check your email!'), type: 'success' })
|
||||
},
|
||||
async submit (e) {
|
||||
e.preventDefault()
|
||||
try {
|
||||
this.loading = true
|
||||
const user = await api.login(this.email, this.password)
|
||||
this.loading = false
|
||||
if (!user) {
|
||||
Message({ message: this.$t('Login error'), type: 'error' })
|
||||
return;
|
||||
}
|
||||
this.login(user)
|
||||
Message({ message: this.$t('Logged'), type: 'success' })
|
||||
} catch (e) {
|
||||
Message({ message: this.$t('Login error'), type: 'error' })
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.email = this.password = ''
|
||||
this.$refs.modal.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
6
pages/README.md
Normal file
6
pages/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# PAGES
|
||||
|
||||
This directory contains your Application Views and Routes.
|
||||
The framework reads all the `*.vue` files inside this directory and creates the router of your application.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing).
|
60
pages/Register.vue
Normal file
60
pages/Register.vue
Normal file
|
@ -0,0 +1,60 @@
|
|||
<template lang='pug'>
|
||||
b-modal(hide-footer @hidden='$router.replace("/")' ref='modal'
|
||||
:title="$t('Register')" :visible='true' @shown='$refs.email.focus()')
|
||||
el-form
|
||||
p(v-html="$t('register_explanation')")
|
||||
el-input.mb-2(ref='email' v-model='user.email' type='email'
|
||||
:placeholder='$t("Email")' autocomplete='email')
|
||||
span(slot='prepend') @
|
||||
|
||||
el-input.mb-2(v-model='user.password' type="password" placeholder="Password")
|
||||
v-icon(name='lock' slot='prepend')
|
||||
|
||||
el-input.mb-2(v-model='user.description' type="textarea" rows='3' :placeholder="$t('Description')")
|
||||
v-icon(name='envelope-open-text')
|
||||
|
||||
|
||||
el-button.float-right(plain type="success" icon='el-icon-arrow-right' @click='register') {{$t('Send')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '~/plugins/api'
|
||||
import { mapActions } from 'vuex';
|
||||
import { Message } from 'element-ui'
|
||||
|
||||
export default {
|
||||
name: 'Register',
|
||||
data () {
|
||||
return {
|
||||
error: {},
|
||||
user: { }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login']),
|
||||
async register () {
|
||||
try {
|
||||
const user = await api.register(this.user)
|
||||
if (!user.is_admin) {
|
||||
this.$refs.modal.hide()
|
||||
Message({
|
||||
message: this.$t('registration_complete'),
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
Message({
|
||||
message: this.$t('admin_registration_complete'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
Message({
|
||||
message: e,
|
||||
type: 'error'
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
121
pages/event/_id.vue
Normal file
121
pages/event/_id.vue
Normal file
|
@ -0,0 +1,121 @@
|
|||
<template lang="pug">
|
||||
b-modal#eventDetail(ref='eventDetail' hide-body hide-header hide-footer @hidden='$router.replace("/")' size='lg' :visible='true')
|
||||
b-card(no-body, :img-src='imgPath' v-loading='loading')
|
||||
el-button.close_button(circle icon='el-icon-close' type='success'
|
||||
@click='$refs.eventDetail.hide()')
|
||||
b-card-header
|
||||
h3 {{event.title}}
|
||||
v-icon(name='clock')
|
||||
span {{event.start_datetime|datetime}}
|
||||
br
|
||||
v-icon(name='map-marker-alt')
|
||||
//- span {{event.place.name}} - {{event.place.address}}
|
||||
br
|
||||
b-card-body(v-if='event.description || event.tags')
|
||||
pre(v-html='event.description')
|
||||
br
|
||||
el-tag.mr-1(:color='tag.color || "grey"' v-for='tag in event.tags'
|
||||
size='mini' :key='tag.tag') {{tag.tag}}
|
||||
.ml-auto(v-if='mine')
|
||||
hr
|
||||
el-button(v-if='event.is_visible' plain type='warning' @click.prevents='toggle' icon='el-icon-view') {{$t('Unconfirm')}}
|
||||
el-button(v-else plain type='success' @click.prevents='toggle' icon='el-icon-view') {{$t('Confirm')}}
|
||||
el-button(plain type='danger' @click.prevent='remove' icon='el-icon-remove') {{$t('Remove')}}
|
||||
el-button(plain type='primary' @click='$router.replace("/edit/"+event.id)') <v-icon color='orange' name='edit'/> {{$t('Edit')}}
|
||||
|
||||
//- COMMENTS ...
|
||||
//- b-navbar(type="dark" variant="dark" toggleable='lg')
|
||||
//- template(slot='footer')
|
||||
//- b-navbar-nav
|
||||
//- b-button(variant='success') {{$t('Share')}} <v-icon name='share'/>
|
||||
//- b-nav-item( {{$t('')}})
|
||||
//- b-card-footer.text-right
|
||||
//- span.mr-3 {{event.comments.length}} <v-icon name='comments'/>
|
||||
//- a(href='#', @click='remove')
|
||||
v-icon(color='orange' name='times')
|
||||
//- b-card-footer(v-for='comment in event.comments')
|
||||
strong {{comment.author}}
|
||||
div(v-html='comment.text')
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import api from '@/plugins/api'
|
||||
//import filters from '@/filters'
|
||||
|
||||
export default {
|
||||
name: 'Event',
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
imgPath () {
|
||||
return this.event.image_path && process.env.VUE_APP_API + '/uploads/' + this.event.image_path
|
||||
},
|
||||
mine () {
|
||||
return this.event.userId === this.user.id || this.user.is_admin
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
event: { comments: [], place: {}, title: ''},
|
||||
id: null,
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.id = this.$route.params.id
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['delEvent']),
|
||||
async load () {
|
||||
const event = await api.getEvent(this.id)
|
||||
this.event = event
|
||||
this.loading = false
|
||||
},
|
||||
async remove () {
|
||||
await api.delEvent(this.event.id)
|
||||
this.delEvent(this.event.id)
|
||||
this.$refs.eventDetail.hide()
|
||||
},
|
||||
async toggle () {
|
||||
try {
|
||||
if (this.event.is_visible) {
|
||||
|
||||
await api.unconfirmEvent(this.id)
|
||||
this.event.is_visible = false
|
||||
} else {
|
||||
await api.confirmEvent(this.id)
|
||||
this.event.is_visible = true
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#eventDetail .modal-body {
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#eventDetail .close_button:hover {
|
||||
background-color: rgba(200, 100, 100, 0.4);
|
||||
}
|
||||
|
||||
#eventDetail .card {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
#eventDetail .close_button {
|
||||
background-color: rgba(100, 100, 100, 0.4);
|
||||
color: red;
|
||||
font-size: 20px;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
2
pages/index.vue
Normal file
2
pages/index.vue
Normal file
|
@ -0,0 +1,2 @@
|
|||
<template lang="pug">
|
||||
</template>
|
215
pages/new_event.vue
Normal file
215
pages/new_event.vue
Normal file
|
@ -0,0 +1,215 @@
|
|||
<template lang="pug">
|
||||
b-modal(ref='modal' @hidden='$router.replace("/")' size='lg' :visible='true'
|
||||
:title="edit?$t('Edit event'):$t('New event')" hide-footer)
|
||||
el-form
|
||||
el-tabs.mb-2(v-model='activeTab' v-loading='sending')
|
||||
|
||||
//- NOT LOGGED EVENT
|
||||
el-tab-pane(v-if='!logged')
|
||||
span(slot='label') {{$t('anon_newevent')}} <v-icon name='user-secret'/>
|
||||
p(v-html="$t('anon_newevent_explanation')")
|
||||
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
|
||||
|
||||
//- WHERE
|
||||
el-tab-pane
|
||||
span(slot='label') {{$t('Where')}} <v-icon name='map-marker-alt'/>
|
||||
div {{$t('where_explanation')}}
|
||||
el-select.mb-3(v-model='event.place.name' @change='placeChoosed' filterable allow-create default-first-option)
|
||||
el-option(v-for='place in places_name' :label='place' :value='place' :key='place.id')
|
||||
div {{$t("Address")}}
|
||||
el-input.mb-3(ref='address' v-model='event.place.address' @keydown.native.enter='next')
|
||||
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
|
||||
|
||||
//- WHEN
|
||||
el-tab-pane
|
||||
span(slot='label') {{$t('When')}} <v-icon name='clock'/>
|
||||
span {{event.multidate ? $t('dates_explanation') : $t('date_explanation')}}
|
||||
el-switch.float-right(v-model='event.multidate' :active-text="$t('multidate_explanation')")
|
||||
v-date-picker.mb-3(:mode='event.multidate ? "range" : "single"' v-model='date' is-inline
|
||||
is-expanded :min-date='new Date()' @input='date ? $refs.time_start.focus() : false')
|
||||
div {{$t('time_start_explanation')}}
|
||||
el-time-select.mb-3(ref='time_start'
|
||||
v-model="time.start"
|
||||
:picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
|
||||
div {{$t('time_end_explanation')}}
|
||||
el-time-select(v-model='time.end'
|
||||
:picker-options="{start: '00:00', step: '00:30', end: '24:00'}")
|
||||
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
|
||||
|
||||
//- WHAT
|
||||
el-tab-pane
|
||||
span(slot='label') {{$t('What')}} <v-icon name='file-alt'/>
|
||||
span {{$t('what_explanation')}}
|
||||
el-input.mb-3(v-model='event.title' ref='title')
|
||||
span {{$t('description_explanation')}}
|
||||
el-input.mb-3(v-model='event.description' type='textarea' :rows='9')
|
||||
span {{$t('tag_explanation')}}
|
||||
br
|
||||
el-select(v-model='event.tags' multiple filterable allow-create
|
||||
default-first-option placeholder='Tag')
|
||||
el-option(v-for='tag in tags' :key='tag.tag'
|
||||
:label='tag' :value='tag')
|
||||
|
||||
el-button.float-right(@click.native='next' :disabled='!couldProceed') {{$t('Next')}}
|
||||
|
||||
el-tab-pane
|
||||
span(slot='label') {{$t('Media')}} <v-icon name='image'/>
|
||||
span {{$t('media_explanation')}}
|
||||
b-form-file.mb-2(v-model='event.image', :placeholder='$t("Poster")' accept='image/*')
|
||||
el-button.float-right(@click='done') {{edit?$t('Edit'):$t('Send')}}
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import api from '@/plugins/api'
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
import moment from 'dayjs'
|
||||
import Calendar from '@/components/Calendar'
|
||||
import { Message } from 'element-ui'
|
||||
export default {
|
||||
components: { Calendar },
|
||||
data() {
|
||||
return {
|
||||
event: {
|
||||
place: { name: '', address: '' },
|
||||
title: '', description: '', tags: [],
|
||||
multidate: false,
|
||||
},
|
||||
visible: true,
|
||||
id: null,
|
||||
activeTab: "0",
|
||||
date: null,
|
||||
time: { start: '20:00', end: null },
|
||||
edit: false,
|
||||
sending: false,
|
||||
}
|
||||
},
|
||||
name: 'newEvent',
|
||||
watch: {
|
||||
'time.start' (value) {
|
||||
let [h, m] = value.split(':')
|
||||
this.time.end = (Number(h)+1) + ':' + m
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
if (this.$route.params.id) {
|
||||
this.id = this.$route.params.id
|
||||
this.edit = true
|
||||
const event = await api.getEvent(this.id)
|
||||
// this.event.place = {name: event.place.name, address: event.place.address }
|
||||
this.event.place.name = event.place.name
|
||||
this.event.place.address = event.place.address || ''
|
||||
this.event.multidate = event.multidate
|
||||
this.date = event.start_datetime
|
||||
this.time.start = moment(event.start_datetime).format('HH:mm')
|
||||
this.time.end = moment(event.end_datetime).format('HH:mm')
|
||||
this.event.title = event.title
|
||||
this.event.description = event.description.replace(/(<([^>]+)>)/ig, '')
|
||||
this.event.id = event.id
|
||||
if (event.tags) {
|
||||
this.event.tags = event.tags.map(t => t.tag)
|
||||
}
|
||||
|
||||
}
|
||||
this.updateMeta()
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
tags: state => state.tags.map(t => t.tag ),
|
||||
places_name: state => state.places.map(p => p.name ),
|
||||
places: state => state.places,
|
||||
user: state => state.user,
|
||||
logged: state => state.logged
|
||||
}),
|
||||
couldProceed () {
|
||||
const t = this.logged ? -1 : 0
|
||||
switch(Number(this.activeTab)) {
|
||||
case 0+t:
|
||||
return true
|
||||
case 1+t:
|
||||
return this.event.place.name.length>0 &&
|
||||
this.event.place.address.length>0
|
||||
case 2+t:
|
||||
if (this.date && this.time.start) return true
|
||||
break
|
||||
case 3+t:
|
||||
return this.event.title.length>0
|
||||
break
|
||||
case 4+t:
|
||||
return true
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['addEvent', 'updateEvent', 'updateMeta']),
|
||||
next () {
|
||||
this.activeTab = String(Number(this.activeTab)+1)
|
||||
if (this.activeTab === "2") {
|
||||
this.$refs.title.focus()
|
||||
}
|
||||
},
|
||||
prev () {
|
||||
this.activeTab = String(Number(this.activeTab-1))
|
||||
},
|
||||
placeChoosed () {
|
||||
const place = this.places.find( p => p.name === this.event.place.name )
|
||||
if (place && place.address) {
|
||||
this.event.place.address = place.address
|
||||
}
|
||||
this.$refs.address.focus()
|
||||
},
|
||||
async done () {
|
||||
let start_datetime, end_datetime
|
||||
const [ start_hour, start_minute ] = this.time.start.split(':')
|
||||
if (!this.time.end) {
|
||||
this.time.end = this.time.start
|
||||
}
|
||||
const [ end_hour, end_minute ] = this.time.end.split(':')
|
||||
if (this.event.multidate) {
|
||||
start_datetime = moment(this.date.start)
|
||||
.set('hour', start_hour).set('minute', start_minute)
|
||||
end_datetime = moment(this.date.end)
|
||||
.set('hour', end_hour).set('minute', end_minute)
|
||||
} else {
|
||||
console.log(this.date)
|
||||
start_datetime = moment(this.date).set('hour', start_hour).set('minute', start_minute)
|
||||
end_datetime = moment(this.date).set('hour', end_hour).set('minute', end_minute)
|
||||
}
|
||||
const formData = new FormData()
|
||||
|
||||
if (this.event.image) {
|
||||
formData.append('image', this.event.image, this.event.image.name)
|
||||
}
|
||||
formData.append('title', this.event.title)
|
||||
formData.append('place_name', this.event.place.name)
|
||||
formData.append('place_address', this.event.place.address)
|
||||
formData.append('description', this.event.description)
|
||||
formData.append('multidate', this.event.multidate)
|
||||
formData.append('start_datetime', start_datetime)
|
||||
formData.append('end_datetime', end_datetime)
|
||||
if (this.edit) {
|
||||
formData.append('id', this.event.id)
|
||||
}
|
||||
if (this.event.tags)
|
||||
this.event.tags.forEach(tag => formData.append('tags[]', tag))
|
||||
this.sending = true
|
||||
try {
|
||||
if (this.edit) {
|
||||
await this.updateEvent(formData)
|
||||
} else {
|
||||
await this.addEvent(formData)
|
||||
}
|
||||
this.updateMeta()
|
||||
this.sending = false
|
||||
this.$refs.modal.hide()
|
||||
Message({ type: 'success', message: this.logged ? this.$t('new_event_added') : this.$t('new_anon_event_added')})
|
||||
} catch (e) {
|
||||
this.sending = false
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
7
plugins/README.md
Normal file
7
plugins/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# PLUGINS
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains Javascript plugins that you want to run before mounting the root Vue.js application.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).
|
82
plugins/api.js
Normal file
82
plugins/api.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import axios from 'axios'
|
||||
import { getters } from '@/store'
|
||||
const api = axios.create({
|
||||
baseURL: process.env.NODE_ENV === 'development' ? 'http://localhost:3000/api' : '/api',
|
||||
withCredentials: true,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
function get(path) {
|
||||
return api.get(path) //, { headers: { 'Authorization': getters.token } })
|
||||
.then(res => res.data)
|
||||
// .catch((e) => {
|
||||
// if (e.response.status === 403) {
|
||||
// // store.commit('logout') // TOFIX
|
||||
// return false
|
||||
// }
|
||||
// throw e.response && e.response.data &&
|
||||
// e.response.data.errors && e.response.data.errors[0].message
|
||||
// })
|
||||
}
|
||||
|
||||
function post(path, data) {
|
||||
return api.post(path, data, { headers: { 'Authorization': getters.token } })
|
||||
.then(res => res.data)
|
||||
// .catch((e) => {
|
||||
// if (e.response.status === 403) {
|
||||
// // store.commit('logout') // TOFIX
|
||||
// return false
|
||||
// }
|
||||
// throw e.response && e.response.data &&
|
||||
// e.response.data.errors && e.response.data.errors[0].message
|
||||
// })
|
||||
}
|
||||
function put(path, data) {
|
||||
return api.put(path, data, { headers: { 'Authorization': getters.token } })
|
||||
.then(ret => ret.data)
|
||||
}
|
||||
|
||||
function del(path) {
|
||||
return api.delete(path, { headers: { 'Authorization': getters.token } }).then(ret => ret.data)
|
||||
}
|
||||
|
||||
export default {
|
||||
login: (email, password) => post('/login', { email, password }),
|
||||
register: user => post('/user', user),
|
||||
|
||||
// password recovery
|
||||
forgotPassword: email => post('/user/recover', { email }),
|
||||
checkRecoverCode: recover_code => post('/user/check_recover_code', { recover_code }),
|
||||
recoverPassword: (recover_code, password) => post('/user/recover_password', { recover_code, password }),
|
||||
|
||||
getAllEvents: (month, year) => get(`/event/${year}/${month}/`),
|
||||
getUnconfirmedEvents: () => get('/event/unconfirmed'),
|
||||
|
||||
confirmEvent: id => get(`/event/confirm/${id}`),
|
||||
unconfirmEvent: id => get(`/event/unconfirm/${id}`),
|
||||
|
||||
addNotification: notification => post('/event/notification', notification),
|
||||
delNotification: code => del(`/event/notification/${code}`),
|
||||
|
||||
addEvent: event => post('/user/event', event),
|
||||
updateEvent: event => put('/user/event', event),
|
||||
|
||||
updatePlace: place => put('/place', place),
|
||||
delEvent: eventId => del(`/user/event/${eventId}`),
|
||||
getEvent: eventId => get(`/event/${eventId}`),
|
||||
getMeta: () => get('/event/meta'),
|
||||
getUser: () => get('/user'),
|
||||
getUsers: () => get('/users'),
|
||||
updateTag: tag => put('/tag', tag),
|
||||
updateUser: user => put('/user', user),
|
||||
getAuthURL: mastodonInstance => post('/user/getauthurl', mastodonInstance),
|
||||
setCode: code => post('/user/code', code),
|
||||
getAdminSettings: () => get('/settings')
|
||||
// setAdminSetting: (key, value) => post('/settings', { key, value })
|
||||
}
|
6
plugins/bootstrap-vue.js
vendored
Normal file
6
plugins/bootstrap-vue.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Vue from 'vue'
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
|
||||
export default () => {
|
||||
Vue.use(BootstrapVue)
|
||||
}
|
26
plugins/element-ui.js
Normal file
26
plugins/element-ui.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import Vue from 'vue'
|
||||
import { Button, Select, Tag, Option, Table, FormItem, Card,
|
||||
Form, Tabs, TabPane, Switch, Input, Loading, TimeSelect,
|
||||
TableColumn, ColorPicker, Pagination, Popover } from 'element-ui'
|
||||
// import locale from 'element-ui/lib/locale/lang/en'
|
||||
|
||||
export default () => {
|
||||
Vue.use(Button)
|
||||
Vue.use(Popover)
|
||||
Vue.use(Card)
|
||||
Vue.use(Select)
|
||||
Vue.use(Tag)
|
||||
Vue.use(Input)
|
||||
Vue.use(Tabs)
|
||||
Vue.use(TabPane)
|
||||
Vue.use(Option)
|
||||
Vue.use(Switch)
|
||||
Vue.use(ColorPicker)
|
||||
Vue.use(Table)
|
||||
Vue.use(TableColumn)
|
||||
Vue.use(Pagination)
|
||||
Vue.use(FormItem)
|
||||
Vue.use(Form)
|
||||
Vue.use(TimeSelect)
|
||||
Vue.use(Loading.directive)
|
||||
}
|
8
plugins/filters.js
Normal file
8
plugins/filters.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import Vue from 'vue'
|
||||
import moment from 'dayjs'
|
||||
import 'dayjs/locale/it'
|
||||
moment.locale('it')
|
||||
|
||||
Vue.filter('datetime', value => moment(value).format('ddd, D MMMM HH:mm'))
|
||||
Vue.filter('short_datetime', value => moment(value).format('D/MM HH:mm'))
|
||||
Vue.filter('hour', value => moment(value).format('HH:mm'))
|
25
plugins/i18n.js
Normal file
25
plugins/i18n.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
export default ({ app, store }) => {
|
||||
// Set i18n instance on app
|
||||
// This way we can use it in middleware and pages asyncData/fetch
|
||||
app.i18n = new VueI18n({
|
||||
locale: store.state.locale,
|
||||
fallbackLocale: 'en'
|
||||
// messages: {
|
||||
// 'en': require('~/locales/en.json'),
|
||||
// 'fr': require('~/locales/fr.json')
|
||||
// }
|
||||
})
|
||||
|
||||
app.i18n.path = (link) => {
|
||||
if (app.i18n.locale === app.i18n.fallbackLocale) {
|
||||
return `/${link}`
|
||||
}
|
||||
|
||||
return `/${app.i18n.locale}/${link}`
|
||||
}
|
||||
}
|
6
plugins/magic-grid.js
Normal file
6
plugins/magic-grid.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Vue from 'vue'
|
||||
import MagicGrid from 'vue-magic-grid'
|
||||
|
||||
export default () => {
|
||||
Vue.use(MagicGrid)
|
||||
}
|
9
plugins/v-calendar.js
Normal file
9
plugins/v-calendar.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import Vue from 'vue'
|
||||
import VCalendar from 'v-calendar'
|
||||
import 'v-calendar/lib/v-calendar.min.css'
|
||||
|
||||
export default () => {
|
||||
Vue.use(VCalendar, {
|
||||
firstDayOfWeek: 2
|
||||
})
|
||||
}
|
24
plugins/vue-awesome.js
Normal file
24
plugins/vue-awesome.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Vue from 'vue'
|
||||
import 'vue-awesome/icons/lock'
|
||||
import 'vue-awesome/icons/user'
|
||||
import 'vue-awesome/icons/plus'
|
||||
import 'vue-awesome/icons/cog'
|
||||
import 'vue-awesome/icons/tools'
|
||||
import 'vue-awesome/icons/file-export'
|
||||
import 'vue-awesome/icons/sign-out-alt'
|
||||
import 'vue-awesome/icons/clock'
|
||||
import 'vue-awesome/icons/map-marker-alt'
|
||||
import 'vue-awesome/icons/file-alt'
|
||||
import 'vue-awesome/icons/image'
|
||||
import 'vue-awesome/icons/tag'
|
||||
import 'vue-awesome/icons/users'
|
||||
import 'vue-awesome/icons/calendar'
|
||||
import 'vue-awesome/icons/edit'
|
||||
import 'vue-awesome/icons/envelope-open-text'
|
||||
import 'vue-awesome/icons/user-secret'
|
||||
import 'vue-awesome/icons/question-circle'
|
||||
import Icon from 'vue-awesome/components/Icon'
|
||||
|
||||
export default () => {
|
||||
Vue.component('v-icon', Icon)
|
||||
}
|
48
server/api/auth.js
Normal file
48
server/api/auth.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
const jwt = require('jsonwebtoken')
|
||||
const { Op } = require('sequelize')
|
||||
const config = require('./config')
|
||||
const User = require('./models/user')
|
||||
|
||||
const Auth = {
|
||||
fillUser(req, res, next) {
|
||||
const token =
|
||||
req.body.token || req.params.token || req.headers['x-access-token']
|
||||
if (!token) return next()
|
||||
jwt.verify(token, config.secret, async (err, decoded) => {
|
||||
if (err) return next()
|
||||
req.user = await User.findOne({
|
||||
where: { email: { [Op.eq]: decoded.email }, is_active: true }
|
||||
})
|
||||
next()
|
||||
})
|
||||
},
|
||||
isAuth(req, res, next) {
|
||||
const token =
|
||||
(req.body && req.body.token) ||
|
||||
req.params.token ||
|
||||
req.headers['x-access-token']
|
||||
if (!token) return res.status(403).send({ message: 'Token not found' })
|
||||
jwt.verify(token, config.secret, async (err, decoded) => {
|
||||
if (err) {
|
||||
return res
|
||||
.status(403)
|
||||
.send({ message: 'Failed to authenticate token ' + err })
|
||||
}
|
||||
req.user = await User.findOne({
|
||||
where: { email: { [Op.eq]: decoded.email }, is_active: true }
|
||||
})
|
||||
if (!req.user) {
|
||||
return res
|
||||
.status(403)
|
||||
.send({ message: 'Failed to authenticate token ' + err })
|
||||
}
|
||||
next()
|
||||
})
|
||||
},
|
||||
isAdmin(req, res, next) {
|
||||
if (req.user.is_admin && req.user.is_active) return next()
|
||||
return res.status(403).send({ message: 'Admin needed' })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Auth
|
27
server/api/config.js
Normal file
27
server/api/config.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/* backend configuration */
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
const db = require('./config/config.json')[env]
|
||||
|
||||
module.exports = {
|
||||
locale: process.env.LOCALE || 'it',
|
||||
title: process.env.TITLE || 'GANCIO',
|
||||
description: process.env.DESCRIPTION || 'A calendar for radical communities',
|
||||
baseurl: process.env.BASE_URL || 'http://localhost:8080',
|
||||
apiurl:
|
||||
env === 'production'
|
||||
? process.env.BASE_URL + '/api'
|
||||
: 'http://localhost:9000',
|
||||
db,
|
||||
admin: process.env.ADMIN_EMAIL,
|
||||
|
||||
smtp: {
|
||||
host: process.env.SMTP_HOST,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
}
|
||||
},
|
||||
|
||||
secret: process.env.SECRET || 'notsosecret'
|
||||
}
|
14
server/api/config/config.json
Normal file
14
server/api/config/config.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"development": {
|
||||
"storage": "/home/les/dev/hacklab/gancio/db.sqlite",
|
||||
"dialect": "sqlite",
|
||||
"logging": false
|
||||
},
|
||||
"production": {
|
||||
"username": "docker",
|
||||
"password": "docker",
|
||||
"database": "gancio",
|
||||
"host": "db",
|
||||
"dialect": "postgres"
|
||||
}
|
||||
}
|
82
server/api/controller/bot.js
Normal file
82
server/api/controller/bot.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
// const { User, Event, Comment, Tag } = require('../model')
|
||||
const config = require('../config')
|
||||
const Mastodon = require('mastodon-api')
|
||||
// const Sequelize = require('sequelize')
|
||||
// const Op = Sequelize.Op
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const moment = require('moment')
|
||||
moment.locale('it')
|
||||
|
||||
const botController = {
|
||||
bots: [],
|
||||
// async initialize () {
|
||||
// console.log('initialize bots')
|
||||
// const botUsers = await User.findAll({ where: { mastodon_auth: { [Op.ne]: null } } })
|
||||
// console.log(botUsers)
|
||||
// botController.bots = botUsers.map(user => {
|
||||
// console.log('initialize bot ', user.name)
|
||||
// console.log('.. ', user.mastodon_auth)
|
||||
// const { client_id, client_secret, access_token } = user.mastodon_auth
|
||||
// const bot = new Mastodon({ access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
|
||||
// const listener = bot.stream('streaming/direct')
|
||||
// listener.on('message', botController.message)
|
||||
// listener.on('error', botController.error)
|
||||
// return { email: user.email, bot }
|
||||
// })
|
||||
// console.log(botController.bots)
|
||||
// },
|
||||
// add (user, token) {
|
||||
// const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
|
||||
// const listener = bot.stream('streaming/direct')
|
||||
// listener.on('message', botController.message)
|
||||
// listener.on('error', botController.error)
|
||||
// botController.bots.push({ email: user.email, bot })
|
||||
// },
|
||||
async post (mastodon_auth, event) {
|
||||
const { access_token, instance } = mastodon_auth
|
||||
const bot = new Mastodon({ access_token, api_url: `https://${instance}/api/v1/` })
|
||||
const status = `${event.title} @ ${event.place.name} ${moment(event.start_datetime).format('ddd, D MMMM HH:mm')} -
|
||||
${event.description.length > 200 ? event.description.substr(0, 200) + '...' : event.description} - ${event.tags.map(t => '#' + t.tag).join(' ')} ${config.baseurl}/event/${event.id}`
|
||||
|
||||
let media
|
||||
if (event.image_path) {
|
||||
const file = path.join(__dirname, '..', '..', 'uploads', event.image_path)
|
||||
if (fs.statSync(file)) {
|
||||
media = await bot.post('media', { file: fs.createReadStream(file) })
|
||||
}
|
||||
}
|
||||
return bot.post('statuses', { status, visibility: 'public', media_ids: media ? [media.data.id] : [] })
|
||||
}
|
||||
// async message (msg) {
|
||||
// console.log(msg)
|
||||
// console.log(msg.data.accounts)
|
||||
// const replyid = msg.data.in_reply_to_id || msg.data.last_status.in_reply_to_id
|
||||
// if (!replyid) return
|
||||
// const event = await Event.findOne({ where: { activitypub_id: replyid } })
|
||||
// if (!event) {
|
||||
// check for comment..
|
||||
// const comment = await Comment.findOne( {where: { }})
|
||||
// }
|
||||
// const comment = await Comment.create({activitypub_id: msg.data.last_status.id, text: msg.data.last_status.content, author: msg.data.accounts[0].username })
|
||||
// event.addComment(comment)
|
||||
// console.log(event)
|
||||
// const comment = await Comment.findOne( { where: {activitypub_id: msg.data.in_reply_to}} )
|
||||
// console.log('dentro message ', data)
|
||||
|
||||
// add comment to specified event
|
||||
// let comment
|
||||
// if (!event) {
|
||||
// const comment = await Comment.findOne({where: {activitypub_id: req.body.id}, include: Event})
|
||||
// event = comment.event
|
||||
// }
|
||||
// const comment = new Comment(req.body)
|
||||
// event.addComment(comment)
|
||||
// },
|
||||
// error (err) {
|
||||
// console.log('error ', err)
|
||||
// }
|
||||
}
|
||||
|
||||
// setTimeout(botController.initialize, 2000)
|
||||
module.exports = botController
|
169
server/api/controller/event.js
Normal file
169
server/api/controller/event.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
const crypto = require('crypto')
|
||||
const moment = require('moment')
|
||||
const { Op } = require('sequelize')
|
||||
const lodash = require('lodash')
|
||||
const { User, Event, Comment, Tag, Place, Notification } = require('../model')
|
||||
|
||||
const eventController = {
|
||||
|
||||
async addComment(req, res) {
|
||||
// comment could be added to an event or to another comment
|
||||
let event = await Event.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } } })
|
||||
if (!event) {
|
||||
const comment = await Comment.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } }, include: Event })
|
||||
event = comment.event
|
||||
}
|
||||
const comment = new Comment(req.body)
|
||||
event.addComment(comment)
|
||||
res.json(comment)
|
||||
},
|
||||
|
||||
async getMeta(req, res) {
|
||||
const places = await Place.findAll()
|
||||
const tags = await Tag.findAll()
|
||||
res.json({ tags, places })
|
||||
},
|
||||
|
||||
async getNotifications(event) {
|
||||
function match(event, filters) {
|
||||
// matches if no filter specified
|
||||
if (!filters) return true
|
||||
|
||||
// check for visibility
|
||||
if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) return false
|
||||
|
||||
if (!filters.tags && !filters.places) return true
|
||||
if (!filters.tags.length && !filters.places.length) return true
|
||||
if (filters.tags.length) {
|
||||
const m = lodash.intersection(event.tags.map(t => t.tag), filters.tags)
|
||||
if (m.length > 0) return true
|
||||
}
|
||||
if (filters.places.length) {
|
||||
if (filters.places.find(p => p === event.place.name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
const notifications = await Notification.findAll()
|
||||
|
||||
// get notification that matches with selected event
|
||||
return notifications.filter(notification => match(event, notification.filters))
|
||||
},
|
||||
|
||||
async updateTag(req, res) {
|
||||
const tag = await Tag.findByPk(req.body.tag)
|
||||
if (tag) {
|
||||
res.json(await tag.update(req.body))
|
||||
} else {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
||||
async updatePlace(req, res) {
|
||||
const place = await Place.findByPk(req.body.id)
|
||||
await place.update(req.body)
|
||||
res.json(place)
|
||||
},
|
||||
|
||||
async get(req, res) {
|
||||
const id = req.params.event_id
|
||||
const event = await Event.findByPk(id, { include: [User, Tag, Comment, Place] })
|
||||
res.json(event)
|
||||
},
|
||||
|
||||
async confirm(req, res) {
|
||||
const id = req.params.event_id
|
||||
const event = await Event.findByPk(id)
|
||||
|
||||
try {
|
||||
await event.update({ is_visible: true })
|
||||
// insert notification
|
||||
const notifications = await eventController.getNotifications(event)
|
||||
await event.setNotifications(notifications)
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
||||
async unconfirm(req, res) {
|
||||
const id = req.params.event_id
|
||||
const event = await Event.findByPk(id)
|
||||
|
||||
try {
|
||||
await event.update({ is_visible: false })
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
||||
async getUnconfirmed(req, res) {
|
||||
const events = await Event.findAll({
|
||||
where: {
|
||||
is_visible: false
|
||||
},
|
||||
order: [['start_datetime', 'ASC']],
|
||||
include: [Tag, Place]
|
||||
})
|
||||
res.json(events)
|
||||
},
|
||||
|
||||
async addNotification(req, res) {
|
||||
try {
|
||||
const notification = {
|
||||
filters: { is_visible: true },
|
||||
email: req.body.email,
|
||||
type: 'mail',
|
||||
remove_code: crypto.randomBytes(16).toString('hex')
|
||||
}
|
||||
await Notification.create(notification)
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
||||
async delNotification(req, res) {
|
||||
const remove_code = req.params.code
|
||||
try {
|
||||
const notification = await Notification.findOne({ where: { remove_code: { [Op.eq]: remove_code } } })
|
||||
await notification.destroy()
|
||||
} catch (e) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
res.sendStatus(200)
|
||||
},
|
||||
|
||||
async getAll(req, res) {
|
||||
console.log('sono qui dentro !')
|
||||
// this is due how v-calendar shows dates
|
||||
const start = moment().year(req.params.year).month(req.params.month)
|
||||
.startOf('month').startOf('isoWeek')
|
||||
let end = moment().year(req.params.year).month(req.params.month).endOf('month')
|
||||
const shownDays = end.diff(start, 'days')
|
||||
if (shownDays <= 34) end = end.add(1, 'week')
|
||||
end = end.endOf('isoWeek')
|
||||
const events = await Event.findAll({
|
||||
// where: {
|
||||
// is_visible: true,
|
||||
// [Op.and]: [
|
||||
// { start_datetime: { [Op.gte]: start } },
|
||||
// { start_datetime: { [Op.lte]: end } }
|
||||
// ]
|
||||
// },
|
||||
// order: [['start_datetime', 'ASC']],
|
||||
// include: [
|
||||
// { model: User, required: false },
|
||||
// Comment,
|
||||
// Tag,
|
||||
// { model: Place, required: false }
|
||||
// ]
|
||||
})
|
||||
console.log(events)
|
||||
res.json(events)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = eventController
|
64
server/api/controller/export.js
Normal file
64
server/api/controller/export.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
const { Event, Comment, Tag, Place } = require('../model')
|
||||
const { Op } = require('sequelize')
|
||||
const config = require('../config')
|
||||
const moment = require('moment')
|
||||
const ics = require('ics')
|
||||
|
||||
const exportController = {
|
||||
|
||||
async export (req, res) {
|
||||
console.log('type ', req.params.type)
|
||||
const type = req.params.type
|
||||
const tags = req.query.tags
|
||||
const places = req.query.places
|
||||
const whereTag = {}
|
||||
const wherePlace = {}
|
||||
const yesterday = moment().subtract('1', 'day')
|
||||
if (tags) {
|
||||
whereTag.tag = tags.split(',')
|
||||
}
|
||||
if (places) {
|
||||
wherePlace.name = places.split(',')
|
||||
}
|
||||
const events = await Event.findAll({
|
||||
where: { is_visible: true, start_datetime: { [Op.gte]: yesterday } },
|
||||
include: [Comment, {
|
||||
model: Tag,
|
||||
where: whereTag
|
||||
}, { model: Place, where: wherePlace } ]
|
||||
})
|
||||
switch (type) {
|
||||
case 'feed':
|
||||
return exportController.feed(res, events.slice(0, 20))
|
||||
case 'ics':
|
||||
return exportController.ics(res, events)
|
||||
}
|
||||
},
|
||||
|
||||
async feed (res, events) {
|
||||
res.type('application/rss+xml; charset=UTF-8')
|
||||
res.render('feed/rss.pug', { events, config, moment })
|
||||
},
|
||||
|
||||
async ics (res, events) {
|
||||
const eventsMap = events.map(e => {
|
||||
const tmpStart = moment(e.start_datetime)
|
||||
const tmpEnd = moment(e.end_datetime)
|
||||
const start = [tmpStart.year(), tmpStart.month() + 1, tmpStart.date(), tmpStart.hour(), tmpStart.minute()]
|
||||
const end = [tmpEnd.year(), tmpEnd.month() + 1, tmpEnd.date(), tmpEnd.hour(), tmpEnd.minute()]
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
title: e.title,
|
||||
description: e.description,
|
||||
location: e.place.name + ' ' + e.place.address
|
||||
}
|
||||
})
|
||||
res.type('text/calendar; charset=UTF-8')
|
||||
const { error, value } = ics.createEvents(eventsMap)
|
||||
console.log(error, value)
|
||||
res.send(value)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exportController
|
27
server/api/controller/settings.js
Normal file
27
server/api/controller/settings.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const { Settings } = require('../model')
|
||||
|
||||
const settingsController = {
|
||||
async setAdminSetting (key, value) {
|
||||
await Settings.findOrCreate({ where: { key },
|
||||
defaults: { value } })
|
||||
.spread((settings, created) => {
|
||||
if (!created) return settings.update({ value })
|
||||
})
|
||||
},
|
||||
|
||||
async getAdminSettings (req, res) {
|
||||
const settings = await settingsController.settings()
|
||||
res.json(settings)
|
||||
},
|
||||
|
||||
async settings () {
|
||||
const settings = await Settings.findAll()
|
||||
const map = {}
|
||||
settings.forEach(setting => {
|
||||
map[setting.key] = setting.value
|
||||
})
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = settingsController
|
283
server/api/controller/user.js
Normal file
283
server/api/controller/user.js
Normal file
|
@ -0,0 +1,283 @@
|
|||
const jwt = require('jsonwebtoken')
|
||||
const Mastodon = require('mastodon-api')
|
||||
|
||||
const User = require('../models/user')
|
||||
const { Event, Tag, Place } = require('../models/event')
|
||||
const settingsController = require('./settings')
|
||||
const eventController = require('./event')
|
||||
const config = require('../config')
|
||||
const mail = require('../mail')
|
||||
const { Op } = require('sequelize')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const userController = {
|
||||
async login (req, res) {
|
||||
// find the user
|
||||
const user = await User.findOne({ where: { email: { [Op.eq]: req.body.email } } })
|
||||
if (!user) {
|
||||
res.status(404).json({ success: false, message: 'AUTH_FAIL' })
|
||||
} else if (user) {
|
||||
if (!user.is_active) {
|
||||
res.status(403).json({ success: false, message: 'NOT_CONFIRMED' })
|
||||
// check if password matches
|
||||
} else if (!await user.comparePassword(req.body.password)) {
|
||||
res.status(403).json({ success: false, message: 'AUTH_FAIL' })
|
||||
} else {
|
||||
// if user is found and password is right
|
||||
// create a token
|
||||
const payload = { email: user.email }
|
||||
var token = jwt.sign(payload, config.secret)
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Enjoy your token!',
|
||||
token,
|
||||
user
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async setToken (req, res) {
|
||||
req.user.mastodon_auth = req.body
|
||||
await req.user.save()
|
||||
res.json(req.user)
|
||||
},
|
||||
|
||||
async delEvent (req, res) {
|
||||
const event = await Event.findByPk(req.params.id)
|
||||
// check if event is mine (or user is admin)
|
||||
if (event && (req.user.is_admin || req.user.id === event.userId)) {
|
||||
if (event.image_path) {
|
||||
const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path)
|
||||
const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path)
|
||||
await fs.unlink(old_path)
|
||||
await fs.unlink(old_thumb_path)
|
||||
}
|
||||
await event.destroy()
|
||||
res.sendStatus(200)
|
||||
} else {
|
||||
res.sendStatus(403)
|
||||
}
|
||||
},
|
||||
|
||||
// ADD EVENT
|
||||
async addEvent (req, res) {
|
||||
const body = req.body
|
||||
|
||||
// remove description tag and create anchor tags
|
||||
const description = body.description
|
||||
.replace(/(<([^>]+)>)/ig, '')
|
||||
.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1">$1</a>')
|
||||
|
||||
const eventDetails = {
|
||||
title: body.title,
|
||||
description,
|
||||
multidate: body.multidate,
|
||||
start_datetime: body.start_datetime,
|
||||
end_datetime: body.end_datetime,
|
||||
is_visible: !!req.user
|
||||
}
|
||||
|
||||
if (req.file) {
|
||||
eventDetails.image_path = req.file.filename
|
||||
}
|
||||
|
||||
let event = await Event.create(eventDetails)
|
||||
|
||||
// create place
|
||||
let place
|
||||
try {
|
||||
place = await Place.findOrCreate({ where: { name: body.place_name },
|
||||
defaults: { address: body.place_address } })
|
||||
.spread((place, created) => place)
|
||||
await event.setPlace(place)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
// create/assign tags
|
||||
if (body.tags) {
|
||||
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
|
||||
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
|
||||
await event.addTags(tags)
|
||||
}
|
||||
if (req.user) await req.user.addEvent(event)
|
||||
event = await Event.findByPk(event.id, { include: [User, Tag, Place] })
|
||||
|
||||
// insert notifications
|
||||
const notifications = await eventController.getNotifications(event)
|
||||
await event.setNotifications(notifications)
|
||||
|
||||
return res.json(event)
|
||||
},
|
||||
|
||||
async updateEvent (req, res) {
|
||||
const body = req.body
|
||||
const event = await Event.findByPk(body.id)
|
||||
if (!req.user.is_admin && event.userId !== req.user.id) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (req.file) {
|
||||
if (event.image_path) {
|
||||
const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path)
|
||||
const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path)
|
||||
await fs.unlink(old_path, e => console.error(e))
|
||||
await fs.unlink(old_thumb_path, e => console.error(e))
|
||||
}
|
||||
body.image_path = req.file.filename
|
||||
}
|
||||
|
||||
body.description = body.description
|
||||
.replace(/(<([^>]+)>)/ig, '') // remove all tags from description
|
||||
.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1">$1</a>') // add links
|
||||
|
||||
await event.update(body)
|
||||
let place
|
||||
try {
|
||||
place = await Place.findOrCreate({ where: { name: body.place_name },
|
||||
defaults: { address: body.place_address } })
|
||||
.spread((place, created) => place)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
await event.setPlace(place)
|
||||
await event.setTags([])
|
||||
if (body.tags) {
|
||||
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
|
||||
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
|
||||
await event.addTags(tags)
|
||||
}
|
||||
const newEvent = await Event.findByPk(event.id, { include: [User, Tag, Place] })
|
||||
return res.json(newEvent)
|
||||
},
|
||||
|
||||
async getAuthURL (req, res) {
|
||||
const instance = req.body.instance
|
||||
const is_admin = req.body.admin && req.user.is_admin
|
||||
const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}`
|
||||
const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`,
|
||||
config.title, 'read write', callback)
|
||||
const url = await Mastodon.getAuthorizationUrl(client_id, client_secret,
|
||||
`https://${instance}`, 'read write', callback)
|
||||
|
||||
if (is_admin) {
|
||||
await settingsController.setAdminSetting('mastodon_auth', { client_id, client_secret, instance })
|
||||
} else {
|
||||
req.user.mastodon_auth = { client_id, client_secret, instance }
|
||||
await req.user.save()
|
||||
}
|
||||
res.json(url)
|
||||
},
|
||||
|
||||
async code (req, res) {
|
||||
const { code, is_admin } = req.body
|
||||
let client_id, client_secret, instance
|
||||
const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}`
|
||||
|
||||
if (is_admin) {
|
||||
const settings = await settingsController.settings();
|
||||
({ client_id, client_secret, instance } = settings.mastodon_auth)
|
||||
} else {
|
||||
({ client_id, client_secret, instance } = req.user.mastodon_auth)
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await Mastodon.getAccessToken(client_id, client_secret, code,
|
||||
`https://${instance}`, callback)
|
||||
const mastodon_auth = { client_id, client_secret, access_token: token, instance }
|
||||
if (is_admin) {
|
||||
await settingsController.setAdminSetting('mastodon_auth', mastodon_auth)
|
||||
res.json(instance)
|
||||
} else {
|
||||
req.user.mastodon_auth = mastodon_auth
|
||||
await req.user.save()
|
||||
// await bot.add(req.user, token)
|
||||
res.json(req.user)
|
||||
}
|
||||
} catch (e) {
|
||||
res.json(e)
|
||||
}
|
||||
},
|
||||
|
||||
async forgotPassword (req, res) {
|
||||
const email = req.body.email
|
||||
const user = await User.findOne({ where: { email: { [Op.eq]: email } } })
|
||||
if (!user) return res.sendStatus(200)
|
||||
|
||||
user.recover_code = crypto.randomBytes(16).toString('hex')
|
||||
mail.send(user.email, 'recover', { user, config })
|
||||
await user.save()
|
||||
res.sendStatus(200)
|
||||
},
|
||||
|
||||
async checkRecoverCode (req, res) {
|
||||
const recover_code = req.body.recover_code
|
||||
if (!recover_code) return res.sendStatus(400)
|
||||
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
|
||||
if (!user) return res.sendStatus(400)
|
||||
res.json(user)
|
||||
},
|
||||
|
||||
async updatePasswordWithRecoverCode (req, res) {
|
||||
const recover_code = req.body.recover_code
|
||||
if (!recover_code) return res.sendStatus(400)
|
||||
const password = req.body.password
|
||||
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
|
||||
if (!user) return res.sendStatus(400)
|
||||
user.password = password
|
||||
await user.save()
|
||||
res.sendStatus(200)
|
||||
},
|
||||
|
||||
async current (req, res) {
|
||||
res.json(req.user)
|
||||
},
|
||||
|
||||
async getAll (req, res) {
|
||||
const users = await User.findAll({
|
||||
order: [['createdAt', 'DESC']]
|
||||
})
|
||||
res.json(users)
|
||||
},
|
||||
|
||||
async update (req, res) {
|
||||
const user = await User.findByPk(req.body.id)
|
||||
if (user) {
|
||||
if (!user.is_active && req.body.is_active) {
|
||||
await mail.send(user.email, 'confirm', { user, config })
|
||||
}
|
||||
await user.update(req.body)
|
||||
res.json(user)
|
||||
} else {
|
||||
res.sendStatus(400)
|
||||
}
|
||||
},
|
||||
|
||||
async register (req, res) {
|
||||
const n_users = await User.count()
|
||||
try {
|
||||
if (n_users === 0) {
|
||||
// the first registered user will be an active admin
|
||||
req.body.is_active = req.body.is_admin = true
|
||||
} else {
|
||||
req.body.is_active = false
|
||||
}
|
||||
const user = await User.create(req.body)
|
||||
try {
|
||||
mail.send([user.email, config.admin], 'register', { user, config })
|
||||
} catch (e) {
|
||||
return res.status(400).json(e)
|
||||
}
|
||||
const payload = { email: user.email }
|
||||
const token = jwt.sign(payload, config.secret)
|
||||
res.json({ user, token })
|
||||
} catch (e) {
|
||||
res.status(404).json(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userController
|
5
server/api/db.js
Normal file
5
server/api/db.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const Sequelize = require('sequelize')
|
||||
const conf = require('./config.js')
|
||||
const db = new Sequelize(conf.db)
|
||||
// db.sync({force: true})
|
||||
module.exports = db
|
86
server/api/index.js
Normal file
86
server/api/index.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
const express = require('express')
|
||||
const multer = require('multer')
|
||||
const { fillUser, isAuth, isAdmin } = require('./auth')
|
||||
const eventController = require('./controller/event')
|
||||
const exportController = require('./controller/export')
|
||||
const userController = require('./controller/user')
|
||||
const settingsController = require('./controller/settings')
|
||||
|
||||
// const botController = require('./controller/bot')
|
||||
|
||||
const storage = require('./storage')({
|
||||
destination: 'uploads/'
|
||||
})
|
||||
|
||||
const upload = multer({ storage })
|
||||
const api = express.Router()
|
||||
// login
|
||||
api.post('/login', userController.login)
|
||||
api.post('/user/recover', userController.forgotPassword)
|
||||
api.post('/user/check_recover_code', userController.checkRecoverCode)
|
||||
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
|
||||
|
||||
api
|
||||
.route('/user')
|
||||
// register
|
||||
.post(userController.register)
|
||||
// get current user
|
||||
.get(isAuth, userController.current)
|
||||
// update user (eg. confirm)
|
||||
.put(isAuth, isAdmin, userController.update)
|
||||
|
||||
// get all users
|
||||
api.get('/users', isAuth, isAdmin, userController.getAll)
|
||||
|
||||
// update a tag (modify color)
|
||||
api.put('/tag', isAuth, isAdmin, eventController.updateTag)
|
||||
|
||||
// update a place (modify address..)
|
||||
api.put('/place', isAuth, isAdmin, eventController.updatePlace)
|
||||
|
||||
api
|
||||
.route('/user/event')
|
||||
// add event
|
||||
.post(fillUser, upload.single('image'), userController.addEvent)
|
||||
// update event
|
||||
.put(isAuth, upload.single('image'), userController.updateEvent)
|
||||
|
||||
// remove event
|
||||
api.delete('/user/event/:id', isAuth, userController.delEvent)
|
||||
|
||||
// get tags/places
|
||||
api.get('/event/meta', eventController.getMeta)
|
||||
|
||||
// get unconfirmed events
|
||||
api.get('/event/unconfirmed', isAuth, isAdmin, eventController.getUnconfirmed)
|
||||
|
||||
// add event notification
|
||||
api.post('/event/notification', eventController.addNotification)
|
||||
api.delete('/event/notification/:code', eventController.delNotification)
|
||||
|
||||
api.get('/settings', settingsController.getAdminSettings)
|
||||
api.post('/settings', settingsController.setAdminSetting)
|
||||
|
||||
// get event
|
||||
api.get('/event/:event_id', eventController.get)
|
||||
|
||||
// confirm event
|
||||
api.get('/event/confirm/:event_id', isAuth, isAdmin, eventController.confirm)
|
||||
api.get(
|
||||
'/event/unconfirm/:event_id',
|
||||
isAuth,
|
||||
isAdmin,
|
||||
eventController.unconfirm
|
||||
)
|
||||
|
||||
// export events (rss/ics)
|
||||
api.get('/export/:type', exportController.export)
|
||||
|
||||
// get events in this range
|
||||
api.get('/event/:year/:month', eventController.getAll)
|
||||
|
||||
// mastodon oauth auth
|
||||
api.post('/user/getauthurl', isAuth, userController.getAuthURL)
|
||||
api.post('/user/code', isAuth, userController.code)
|
||||
|
||||
module.exports = api
|
44
server/api/mail.js
Normal file
44
server/api/mail.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
const Email = require('email-templates')
|
||||
const path = require('path')
|
||||
const config = require('./config')
|
||||
const moment = require('moment')
|
||||
moment.locale('it')
|
||||
|
||||
const mail = {
|
||||
send (addresses, template, locals) {
|
||||
const email = new Email({
|
||||
views: { root: path.join(__dirname, 'emails') },
|
||||
juice: true,
|
||||
juiceResources: {
|
||||
preserveImportant: true,
|
||||
webResources: {
|
||||
relativeTo: path.join(__dirname, 'emails')
|
||||
}
|
||||
},
|
||||
message: {
|
||||
from: `${config.title} <${config.smtp.auth.user}>`
|
||||
},
|
||||
send: true,
|
||||
i18n: {
|
||||
locales: ['en', 'es', 'it'],
|
||||
defaultLocale: config.locale
|
||||
},
|
||||
transport: config.smtp
|
||||
})
|
||||
return email.send({
|
||||
template,
|
||||
message: {
|
||||
to: addresses,
|
||||
bcc: config.admin
|
||||
},
|
||||
locals: {
|
||||
...locals,
|
||||
locale: config.locale,
|
||||
config,
|
||||
datetime: datetime => moment(datetime).format('ddd, D MMMM HH:mm')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = mail
|
14
server/api/model.js
Normal file
14
server/api/model.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
const User = require('./models/user')
|
||||
const { Event, Comment, Tag, Place, Notification, EventNotification } = require('./models/event')
|
||||
const Settings = require('./models/settings')
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Event,
|
||||
Comment,
|
||||
Tag,
|
||||
Place,
|
||||
Notification,
|
||||
EventNotification,
|
||||
Settings
|
||||
}
|
72
server/api/models/event.js
Normal file
72
server/api/models/event.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
const Sequelize = require('sequelize')
|
||||
const db = require('../db')
|
||||
const User = require('./user')
|
||||
|
||||
const Event = db.define('event', {
|
||||
title: Sequelize.STRING,
|
||||
description: Sequelize.TEXT,
|
||||
multidate: Sequelize.BOOLEAN,
|
||||
start_datetime: { type: Sequelize.DATE, index: true },
|
||||
end_datetime: { type: Sequelize.DATE, index: true },
|
||||
image_path: Sequelize.STRING,
|
||||
activitypub_id: { type: Sequelize.INTEGER, index: true },
|
||||
is_visible: Sequelize.BOOLEAN
|
||||
})
|
||||
|
||||
const Tag = db.define('tag', {
|
||||
tag: { type: Sequelize.STRING, index: true, unique: true, primaryKey: true },
|
||||
color: { type: Sequelize.STRING }
|
||||
})
|
||||
|
||||
const Comment = db.define('comment', {
|
||||
activitypub_id: { type: Sequelize.INTEGER, index: true },
|
||||
author: Sequelize.STRING,
|
||||
text: Sequelize.STRING
|
||||
})
|
||||
|
||||
const Notification = db.define('notification', {
|
||||
filters: Sequelize.JSON,
|
||||
email: Sequelize.STRING,
|
||||
remove_code: Sequelize.STRING,
|
||||
type: {
|
||||
type: Sequelize.ENUM,
|
||||
values: ['mail', 'admin_email', 'mastodon']
|
||||
}
|
||||
})
|
||||
|
||||
const Place = db.define('place', {
|
||||
name: { type: Sequelize.STRING, unique: true, index: true },
|
||||
address: { type: Sequelize.STRING }
|
||||
})
|
||||
|
||||
Comment.belongsTo(Event)
|
||||
Event.hasMany(Comment)
|
||||
|
||||
Event.belongsToMany(Tag, { through: 'tagEvent' })
|
||||
Tag.belongsToMany(Event, { through: 'tagEvent' })
|
||||
|
||||
const EventNotification = db.define('EventNotification', {
|
||||
status: {
|
||||
type: Sequelize.ENUM,
|
||||
values: ['new', 'sent', 'error'],
|
||||
defaultValue: 'new',
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
Event.belongsToMany(Notification, { through: EventNotification })
|
||||
Notification.belongsToMany(Event, { through: EventNotification })
|
||||
|
||||
Event.belongsTo(User)
|
||||
Event.belongsTo(Place)
|
||||
|
||||
User.hasMany(Event)
|
||||
Place.hasMany(Event)
|
||||
|
||||
// async function init() {
|
||||
// await Notification.findOrCreate({ where: { type: 'mastodon', filters: { is_visible: true } } })
|
||||
// await Notification.findOrCreate({ where: { type: 'admin_email', filters: { is_visible: false } } })
|
||||
// }
|
||||
|
||||
// init()
|
||||
module.exports = { Event, Comment, Tag, Place, Notification, EventNotification }
|
37
server/api/models/index.js
Normal file
37
server/api/models/index.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Sequelize = require('sequelize');
|
||||
const basename = path.basename(__filename);
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const config = require(__dirname + '/../config/config.json')[env];
|
||||
const db = {};
|
||||
|
||||
let sequelize;
|
||||
if (config.use_env_variable) {
|
||||
sequelize = new Sequelize(process.env[config.use_env_variable], config);
|
||||
} else {
|
||||
sequelize = new Sequelize(config.database, config.username, config.password, config);
|
||||
}
|
||||
|
||||
fs
|
||||
.readdirSync(__dirname)
|
||||
.filter(file => {
|
||||
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
|
||||
})
|
||||
.forEach(file => {
|
||||
const model = sequelize['import'](path.join(__dirname, file));
|
||||
db[model.name] = model;
|
||||
});
|
||||
|
||||
Object.keys(db).forEach(modelName => {
|
||||
if (db[modelName].associate) {
|
||||
db[modelName].associate(db);
|
||||
}
|
||||
});
|
||||
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
9
server/api/models/settings.js
Normal file
9
server/api/models/settings.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const db = require('../db')
|
||||
const Sequelize = require('sequelize')
|
||||
|
||||
const Settings = db.define('settings', {
|
||||
key: { type: Sequelize.STRING, primaryKey: true, index: true },
|
||||
value: Sequelize.JSON
|
||||
})
|
||||
|
||||
module.exports = Settings
|
34
server/api/models/user.js
Normal file
34
server/api/models/user.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const bcrypt = require('bcrypt')
|
||||
const db = require('../db')
|
||||
const Sequelize = require('sequelize')
|
||||
|
||||
const User = db.define('user', {
|
||||
email: {
|
||||
type: Sequelize.STRING,
|
||||
unique: { msg: 'Email already exists' },
|
||||
index: true,
|
||||
allowNull: false
|
||||
},
|
||||
description: Sequelize.TEXT,
|
||||
password: Sequelize.STRING,
|
||||
recover_code: Sequelize.STRING,
|
||||
is_admin: Sequelize.BOOLEAN,
|
||||
is_active: Sequelize.BOOLEAN,
|
||||
mastodon_auth: Sequelize.JSON
|
||||
})
|
||||
|
||||
User.prototype.comparePassword = async function (pwd) {
|
||||
if (!this.password) return false
|
||||
const ret = await bcrypt.compare(pwd, this.password)
|
||||
return ret
|
||||
}
|
||||
|
||||
User.beforeSave(async (user, options) => {
|
||||
if (user.changed('password')) {
|
||||
const salt = await bcrypt.genSalt(10)
|
||||
const hash = await bcrypt.hash(user.password, salt)
|
||||
user.password = hash
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = User
|
62
server/api/storage.js
Normal file
62
server/api/storage.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
const mkdirp = require('mkdirp')
|
||||
const sharp = require('sharp')
|
||||
|
||||
function getDestination(req, file, cb) {
|
||||
cb(null, os.tmpdir())
|
||||
}
|
||||
|
||||
function DiskStorage(opts) {
|
||||
if (typeof opts.destination === 'string') {
|
||||
mkdirp.sync(opts.destination)
|
||||
this.getDestination = function ($0, $1, cb) { cb(null, opts.destination) }
|
||||
} else {
|
||||
this.getDestination = (opts.destination || getDestination)
|
||||
}
|
||||
}
|
||||
|
||||
DiskStorage.prototype._handleFile = function _handleFile(req, file, cb) {
|
||||
const that = this
|
||||
that.getDestination(req, file, function (err, destination) {
|
||||
if (err) return cb(err)
|
||||
|
||||
const filename = crypto.randomBytes(16).toString('hex') + '.jpg'
|
||||
const finalPath = path.join(destination, filename)
|
||||
const thumbPath = path.join(destination, 'thumb', filename)
|
||||
const outStream = fs.createWriteStream(finalPath)
|
||||
const thumbStream = fs.createWriteStream(thumbPath)
|
||||
const resizer = sharp().resize(800).jpeg({ quality: 80 })
|
||||
const thumbnailer = sharp().resize(400).jpeg({ quality: 60 })
|
||||
|
||||
file.stream.pipe(thumbnailer).pipe(thumbStream)
|
||||
thumbStream.on('error', e => console.log('thumbStream error ', e))
|
||||
|
||||
file.stream.pipe(resizer).pipe(outStream)
|
||||
outStream.on('error', cb)
|
||||
outStream.on('finish', function () {
|
||||
cb(null, {
|
||||
destination,
|
||||
filename,
|
||||
path: finalPath,
|
||||
size: outStream.bytesWritten
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
DiskStorage.prototype._removeFile = function _removeFile(req, file, cb) {
|
||||
let path = file.path
|
||||
|
||||
delete file.destination
|
||||
delete file.filename
|
||||
delete file.path
|
||||
|
||||
fs.unlink(path, cb)
|
||||
}
|
||||
|
||||
module.exports = function (opts) {
|
||||
return new DiskStorage(opts)
|
||||
}
|
36
server/index.js
Normal file
36
server/index.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
const express = require('express')
|
||||
const consola = require('consola')
|
||||
const { Nuxt, Builder } = require('nuxt')
|
||||
const app = express()
|
||||
const cors = require('cors')
|
||||
|
||||
// Import and Set Nuxt.js options
|
||||
const config = require('../nuxt.config.js')
|
||||
config.dev = !(process.env.NODE_ENV === 'production')
|
||||
|
||||
async function start() {
|
||||
// Init Nuxt.js
|
||||
const nuxt = new Nuxt(config)
|
||||
|
||||
const { host, port } = nuxt.options.server
|
||||
|
||||
// Build only in dev mode
|
||||
if (config.dev) {
|
||||
const builder = new Builder(nuxt)
|
||||
await builder.build()
|
||||
} else {
|
||||
await nuxt.ready()
|
||||
}
|
||||
|
||||
// Give nuxt middleware to express
|
||||
app.use(cors())
|
||||
app.use(nuxt.render)
|
||||
|
||||
// Listen the server
|
||||
app.listen(port, host)
|
||||
consola.ready({
|
||||
message: `Server listening on http://${host}:${port}`,
|
||||
badge: true
|
||||
})
|
||||
}
|
||||
start()
|
11
static/README.md
Normal file
11
static/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# STATIC
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains your static files.
|
||||
Each file inside this directory is mapped to `/`.
|
||||
Thus you'd want to delete this README.md before deploying to production.
|
||||
|
||||
Example: `/static/robots.txt` is mapped as `/robots.txt`.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static).
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
10
store/README.md
Normal file
10
store/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# STORE
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains your Vuex Store files.
|
||||
Vuex Store option is implemented in the Nuxt.js framework.
|
||||
|
||||
Creating a file in this directory automatically activates the option in the framework.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
|
167
store/index.js
Normal file
167
store/index.js
Normal file
|
@ -0,0 +1,167 @@
|
|||
import moment from 'dayjs'
|
||||
import { intersection } from 'lodash'
|
||||
import api from '~/plugins/api'
|
||||
import Vue from 'vue'
|
||||
|
||||
Vue.config.errorHandler = function (err, vm, info) {
|
||||
// handle error
|
||||
// `info` is a Vue-specific error info, e.g. which lifecycle hook
|
||||
// the error was found in. Only available in 2.2.0+
|
||||
console.error(err)
|
||||
console.error(info)
|
||||
}
|
||||
|
||||
export const state = () => ({
|
||||
events: [],
|
||||
user: {},
|
||||
logged: false,
|
||||
token: '',
|
||||
tags: [],
|
||||
places: [],
|
||||
filters: {
|
||||
tags: [],
|
||||
places: []
|
||||
}
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
token: state => state.token,
|
||||
// filter current + future events only
|
||||
// plus, filter matches search tag/place
|
||||
filteredEvents: (state) => {
|
||||
const events = state.events.map((e) => {
|
||||
const end_datetime = e.end_datetime || moment(e.start_datetime).add('3', 'hour')
|
||||
const past = (moment().diff(end_datetime, 'minutes') > 0)
|
||||
e.past = past
|
||||
return e
|
||||
})
|
||||
if (!state.filters.tags.length && !state.filters.places.length) {
|
||||
return events
|
||||
}
|
||||
return events.filter((e) => {
|
||||
if (state.filters.tags.length) {
|
||||
const m = intersection(e.tags.map(t => t.tag), state.filters.tags)
|
||||
if (m.length > 0) return true
|
||||
}
|
||||
if (state.filters.places.length) {
|
||||
if (state.filters.places.find(p => p === e.place.name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
logout(state) {
|
||||
state.logged = false
|
||||
state.token = ''
|
||||
state.user = {}
|
||||
},
|
||||
login(state, user) {
|
||||
state.logged = true
|
||||
state.user = user.user
|
||||
state.token = user.token
|
||||
},
|
||||
setEvents(state, events) {
|
||||
state.events = events
|
||||
},
|
||||
addEvent(state, event) {
|
||||
state.events.push(event)
|
||||
},
|
||||
updateEvent(state, event) {
|
||||
state.events = state.events.map((e) => {
|
||||
if (e.id !== event.id) return e
|
||||
return event
|
||||
})
|
||||
},
|
||||
delEvent(state, eventId) {
|
||||
state.events = state.events.filter(ev => ev.id !== eventId)
|
||||
},
|
||||
update(state, { tags, places }) {
|
||||
state.tags = tags
|
||||
state.places = places
|
||||
},
|
||||
// search
|
||||
addSearchTag(state, tag) {
|
||||
if (!state.filters.tags.find(t => t === tag.tag)) {
|
||||
state.filters.tags.push(tag.tag)
|
||||
} else {
|
||||
state.filters.tags = state.filters.tags.filter(t => t !== tag.tag)
|
||||
}
|
||||
},
|
||||
setSearchTags(state, tags) {
|
||||
state.filters.tags = tags
|
||||
},
|
||||
addSearchPlace(state, place) {
|
||||
if (state.filters.places.find(p => p.name === place.name)) {
|
||||
state.filters.places.push(place)
|
||||
}
|
||||
},
|
||||
setSearchPlaces(state, places) {
|
||||
state.filters.places = places
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
// called on server request
|
||||
// get current month's event
|
||||
async nuxtServerInit({ commit }, { req }) {
|
||||
// set user if logged! TODO
|
||||
|
||||
const now = new Date()
|
||||
const events = await api.getAllEvents(now.getMonth() - 1, now.getFullYear())
|
||||
commit('setEvents', events)
|
||||
},
|
||||
async updateEvents({ commit }, date) {
|
||||
console.log('dentro updateEvents ', date.month, api)
|
||||
try {
|
||||
const events = await api.getAllEvents(date.month - 1, date.year)
|
||||
console.log('dopo getAll events', events)
|
||||
commit('setEvents', events)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
async updateMeta({ commit }) {
|
||||
const { tags, places } = await api.getMeta()
|
||||
commit('update', { tags, places })
|
||||
},
|
||||
async addEvent({ commit }, formData) {
|
||||
const event = await api.addEvent(formData)
|
||||
if (this.state.logged) {
|
||||
commit('addEvent', event)
|
||||
}
|
||||
},
|
||||
async updateEvent({ commit }, formData) {
|
||||
const event = await api.updateEvent(formData)
|
||||
commit('updateEvent', event)
|
||||
},
|
||||
delEvent({ commit }, eventId) {
|
||||
commit('delEvent', eventId)
|
||||
},
|
||||
login({ commit }, user) {
|
||||
commit('login', user)
|
||||
},
|
||||
logout({ commit }) {
|
||||
commit('logout')
|
||||
},
|
||||
// search
|
||||
addSearchTag({ commit }, tag) {
|
||||
commit('addSearchTag', tag)
|
||||
},
|
||||
setSearchTags({ commit }, tags) {
|
||||
commit('setSearchTags', tags)
|
||||
},
|
||||
addSearchPlace({ commit }, place) {
|
||||
commit('addSearchPlace', place)
|
||||
},
|
||||
setSearchPlaces({ commit }, places) {
|
||||
commit('setSearchPlaces', places)
|
||||
}
|
||||
}
|
||||
// export const getters = {
|
||||
// filteredEvents: state => state.events
|
||||
// }
|
Loading…
Reference in a new issue