Merge branch 'nuxt'

This commit is contained in:
lesion
2019-06-08 13:28:15 +02:00
96 changed files with 4852 additions and 0 deletions

13
.editorconfig Normal file
View 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

28
.eslintrc.js Normal file
View File

@@ -0,0 +1,28 @@
module.exports = {
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: 'babel-eslint'
},
extends: [
'@nuxtjs',
// 'plugin:nuxt/recommended',
// 'plugin:prettier/recommended',
// 'prettier',
// 'prettier/vue'
],
plugins: [
'prettier'
],
// add your custom rules here
rules: {
'nuxt/no-cjs-in-config': 'off',
'camelcase': 'off',
'no-console': 'off',
'arrow-parens': 'off',
'import/order': 'off'
}
}

89
.gitignore vendored Normal file
View File

@@ -0,0 +1,89 @@
# Created by .ignore support plugin (hsz.mobi)
### Gancio production configuration
config.js
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
uploads
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE
.idea
# Service worker
sw.*

4
.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

7
.sequelizerc Normal file
View File

@@ -0,0 +1,7 @@
const path = require('path')
module.exports = {
'config': path.resolve('config.js'),
'migrations-path': path.resolve('server', 'migrations'),
'models-path': path.resolve('server', 'api', 'models')
}

331
.vscode/vscode-kanban.json vendored Normal file
View File

@@ -0,0 +1,331 @@
{
"done": [
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:47:35.777Z",
"id": "5",
"prio": 3,
"references": [],
"title": "server side auth"
},
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:50:00.973Z",
"description": {
"content": "- export page ok\n- usare un'altra api per retrieve di eventi (perche' devo mostrarli tutti, non solo quelli del mese corrente)\n- non devo fare il load degli eventi nel nuxtServerInit (o dentro il layout normale oppure nelle pagine, tipo nella home)",
"mime": "text/markdown"
},
"details": {
"content": "- export page ok\n- usare un'altra api per retrieve di eventi (perche' devo mostrarli tutti, non solo quelli del mese corrente)\n- non devo fare il load degli eventi nel nuxtServerInit (o dentro il layout normale oppure nelle pagine, tipo nella home)\n",
"mime": "text/markdown"
},
"id": "7",
"prio": 1,
"references": [],
"title": "export page",
"type": "bug"
},
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:55:59.993Z",
"id": "10",
"prio": 1,
"references": [],
"title": "gestione errori form aggiungi evento",
"type": "bug"
},
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:56:46.263Z",
"id": "11",
"prio": 1,
"references": [],
"title": "get comments / media from mastodon"
},
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:46:46.332Z",
"id": "3",
"prio": 0,
"references": [],
"title": "export lista"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-04-30T12:46:42.208Z",
"id": "16",
"references": [],
"title": "riesco a rimuovere bootstrap-vue e usare solo element ?"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-04-28T09:25:50.701Z",
"id": "13",
"references": [],
"title": "test altra visualizzazione"
}
],
"in-progress": [
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-04-30T22:00:29.237Z",
"id": "17",
"references": [],
"title": "porcoddio la config arriva anche al client ovviamente, devo separare!"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-04-30T12:31:20.056Z",
"id": "15",
"references": [],
"title": "test deploy"
}
],
"testing": [],
"todo": [
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:50:55.458Z",
"id": "8",
"prio": 1,
"references": [],
"title": "rivedere ux / messaggi utente",
"type": "bug"
},
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:45:27.613Z",
"description": {
"content": "probabilmente devo far diventare il campo senza timezone",
"mime": "text/markdown"
},
"id": "2",
"prio": 0,
"references": [],
"title": "check date timezone",
"type": "bug"
},
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:48:54.407Z",
"description": {
"content": "probabilmente lato client dovrei aggiungere una classe css al body per capire se js e' attivo o meno e poi lavorare di css",
"mime": "text/markdown"
},
"id": "6",
"prio": 0,
"references": [],
"title": "risolvere le modali quando il js e' disabilitato",
"type": "bug"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-06-01T21:00:22.155Z",
"id": "28",
"references": [],
"title": "activitypub stream"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-05-27T15:42:35.467Z",
"id": "21",
"references": [],
"title": "all'admin deve mostrare un badge se ci sono pending operation"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-05-27T15:16:22.839Z",
"id": "20",
"references": [],
"title": "check password reset"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-06-01T21:23:46.941Z",
"id": "30",
"references": [],
"title": "choose listening port"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-05-27T15:45:39.093Z",
"id": "23",
"references": [],
"title": "colori te prego!"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-05-27T20:42:22.581Z",
"id": "24",
"references": [],
"title": "copy to clipboard"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-05-29T13:08:20.887Z",
"id": "25",
"references": [],
"title": "creazione script di backup"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-06-01T21:00:07.431Z",
"id": "27",
"references": [],
"title": "eventi ricorrenti"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-05-27T15:45:18.012Z",
"id": "22",
"references": [],
"title": "filtri per luogo!"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-04-29T10:01:01.632Z",
"id": "14",
"references": [],
"title": "gestione errori quando non c'e' un evento"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-05-02T16:15:07.106Z",
"id": "19",
"references": [],
"title": "modifica eventi multigiorno"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-05-02T11:50:28.476Z",
"id": "18",
"references": [],
"title": "notifiche email / iscrizione / evento da confermare"
},
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:44:56.705Z",
"id": "1",
"references": [],
"title": "popup sul calendario"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-04-27T19:44:33.769Z",
"id": "12",
"references": [],
"title": "rifare il calendario o solo il popup"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-06-01T21:15:42.190Z",
"id": "29",
"references": [],
"title": "settings di istanza (default filter, eg, eventi ricorrenti)"
},
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:47:10.704Z",
"id": "4",
"prio": 0,
"references": [],
"title": "traduzione in inglese"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-05-29T13:10:04.463Z",
"id": "26",
"references": [],
"title": "v-calendar colori e eventi multidays..."
},
{
"assignedTo": {
"name": "lesion"
},
"category": "feature",
"creation_time": "2019-04-23T19:51:05.917Z",
"id": "9",
"prio": -1,
"references": [],
"title": "documentare sorgenti",
"type": "bug"
},
{
"assignedTo": {
"name": "lesion"
},
"creation_time": "2019-06-05T20:39:59.287Z",
"id": "31",
"references": [],
"title": "scroll h su mobile"
}
]
}

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
## gancio
### event manager for radical communities
> :warning: Gancio is under heavy development,
> if something is not working as expected, it's expected :D
## Install
You will need `npm` or `yarn` installed in your system.
``` bash
# clone this repo
git clone https://git.lattuga.net/cisti/gancio.git
cd gancio
# install dependencies
yarn install
# edit configuration
cp config.example.js config.js
# - migrate/create test sqlite db
yarn migrate:dev
# testing with sqlite db
yarn dev
# - migrate/create production db
yarn migrate
# build for production and launch server
yarn build
yarn start
```
##### nginx setup
https://nuxtjs.org/faq/nginx-proxy
For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org).
## Hacking
``` bash
$ yarn dev
```

7
assets/README.md Normal file
View 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).

73
assets/style.less Normal file
View File

@@ -0,0 +1,73 @@
@background: #222C32;
@success: #c7ffbc;
// @info
html, body {
margin: 0px;
background-color: @background !important;
width: 100%;
overflow-x: hidden;
box-sizing: border-box;
font-family: BlinkMacSystemFont,-apple-system,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif !important;
}
* {
box-sizing: border-box;
}
.el-form-item {
margin-bottom: 5px;
}
.el-divider__text {
background-color: @background;
color: white;
border-radius: 5px;
}
.el-card {
max-width: 600px;
margin: 30px auto;
}
.el-dialog {
margin-top: 0px !important;
border-radius: 0px;
width: 100%;
.el-dialog__body {
word-break: break-word;
}
}
.el-select-dropdown {
max-width: 100%;
left: 0px;
}
.page-enter-active, .page-leave-active {
transition: opacity .2s, transform .3s;
}
.page-enter, .page-leave-active {
transition: opacity .3s, transform .2s;
opacity: 0;
// transform: translateY(30px);
}
pre {
font-family: BlinkMacSystemFont,-apple-system,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif !important;
margin-bottom: 0px;
white-space: pre-line;
font-family: unset;
}
.el-popover {
word-break: normal;
}
@media only screen and (min-width: 768px) {
.el-dialog {
margin-top: 10vh !important;
width: 700px;
border-radius: 3px;
}
}

95
components/Calendar.vue Normal file
View File

@@ -0,0 +1,95 @@
<template lang="pug">
#calendar
v-calendar(
title-position='left'
locale='it'
is-dark
:attributes='attributes'
:from-page.sync='page'
is-expanded
is-inline
@dayclick='click')
</template>
<script>
import { mapState, mapActions, mapGetters } from 'vuex'
import moment from 'dayjs'
import { intersection, sample, get } from 'lodash'
export default {
name: 'Calendar',
data () {
const month = moment().month()+1
const year = moment().year()
return {
page: { month, year},
}
},
watch: {
page () {
this.updateEvents(this.page)
}
},
methods: {
...mapActions(['updateEvents']),
click (day) {
const element = document.getElementById(day.day)
if (element) element.scrollIntoView(); //Even IE6 supports this
},
eventToAttribute(event) {
let e = {
key: event.id,
customData: event,
order: event.start_datetime,
}
const day = moment(event.start_datetime).date()
let color = event.past ? 'rgba(200,200,200,0.5)' : get(event, 'tags[0].color') || 'rgba(170,170,250,0.7)'
console.error(color)
if (event.multidate) {
e.dates = {
start: event.start_datetime, end: event.end_datetime
}
e.highlight = {
color: sample(['purple', 'red', 'green', 'blue']),
}
} else {
e.dates = event.start_datetime
e.dot = { color: sample(['purple', 'red', 'green', 'blue']) }
}
return e
}
},
computed: {
...mapGetters(['filteredEvents']),
...mapState(['events']),
attributes () {
return [
{ key: 'today', dates: new Date(),
highlight: {
backgroundColor: '#aaffaa'
},
},
...this.filteredEvents.map(this.eventToAttribute)
]
}
}
}
</script>
<style>
#calendar {
margin: 0 auto;
max-width: 500px;
align-self: center;
}
.vc-highlight {
/* color: red; */
height: 22px !important;
opacity: 0.4;
border-radius: 15px;
}
</style>

138
components/Event.vue Normal file
View File

@@ -0,0 +1,138 @@
<template lang="pug">
nuxt-link.event(:to='`event/${event.id}`' :class='{ withImg: event.image_path }')
//- image
img(v-if='showImage && event.image_path' :src='`/media/thumb/${event.image_path}`')
.event-info
.content-info
//- title
h2 {{event.title}}
//- date / place
.date
div {{event|event_when}}
div @{{event.place.name}}
//- p(v-if='showDescription') {{event.description}}
//- div(v-if='event.comments && event.comments.length')
//- v-icon(name='comments' color='dark')
//- span {{event.comments.length}} {{$t('common.comments')}}
ul.tags(v-if='showTags && event.tags')
li(v-for='tag in event.tags' :key='tag.tag') {{tag.tag}}
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
props: {
event: Object,
showTags: {
type: Boolean,
default: true
},
showImage: {
type: Boolean,
default: true
},
showDescription: {
type: Boolean,
default: true
},
selected: {
type: Boolean,
default: false
}
},
computed: {
date () {
return new Date(this.event.start_datetime).getDate()
}
}
}
</script>
<style lang='less'>
@import '../assets/style.less';
@media only screen and (min-width: 574px) {
.event {
height: 100%;
}
}
.event {
padding: 3px;
display: flex;
flex-direction: column;
// height: 100%;
&:hover {
text-decoration: none;
}
img {
width: 100%;
max-height: 200px;
object-fit: cover;
object-position: top;
}
.event-info {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #111214;
}
.content-info {
padding: 0.8em 1em;
h2 {
color: @success;
font-size: 16px;
font-size: 1.2rem;
font-weight: 400;
margin: 0px;
}
p {
max-height: 92px;
overflow: hidden;
color: white;
margin: 0px;
}
.date {
font-weight: 300;
font-size: 12px;
font-size: 0.95rem;
color: #ff917a;
}
}
.tags {
font-size: 12px;
padding: 1px;
margin-bottom: 0;
display:flex;
flex-wrap: wrap;
justify-content: center;
li {
background: #1B1F21;
display: inline-block;
padding: 2px 10px;
color: rgba(255,255,255,0.7);
margin: 1px;
text-align: center;
flex-grow: 1;
}
}
}
</style>

57
components/Home.vue Normal file
View File

@@ -0,0 +1,57 @@
<template lang="pug">
section
a(href='#totop')
el-button.top.d-block.d-sm-none(icon='el-icon-top' circle type='primary' plain)
a.totop(name='totop')
no-ssr
Calendar
.row.m-0
.p-0.col-sm-6.col-lg-4.col-xl-3(v-for='event in filteredEvents')
a(:id='event.newDay' v-if='event.newDay')
.d-block.d-sm-none
el-divider {{event.start_datetime|day}}
Event(
:id='event.start_datetime'
:key='event.id'
:event='event'
)
</template>
<script>
import { mapGetters } from 'vuex'
import Event from '@/components/Event'
import Calendar from '@/components/Calendar'
export default {
name: 'Home',
data () {
return { }
},
components: { Calendar, Event },
computed: mapGetters(['filteredEvents']),
}
</script>
<style lang="less">
section {
width: 100%;
max-width: 1500px;
margin: 0 auto;
.top {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 1;
opacity: 0.7;
font-size: 16px;
}
.totop {
position: absolute;
top: 0px;
}
}
</style>

82
components/List.vue Normal file
View File

@@ -0,0 +1,82 @@
<template lang="pug">
div#list
el-divider {{title}}
el-timeline
el-timeline-item(
v-for='event in events'
:key='event.id'
:timestamp='event|event_when'
placement='top' icon='el-icon-arrow-down' size='large'
)
div.float-right
small @{{event.place.name}}
a(:href='"/event/" + event.id' target='_blank') {{event.title}}
hr
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'List',
data () {
return { }
},
props: {
title: {
type: String,
default: ''
},
events: {
type: Array,
default: () => {
return []
}
},
maxEvents: {
type: Number,
default: 0
},
minimal: {
type: Boolean,
default: false
},
showTags: {
type: Boolean,
default: true,
},
showImage: {
type: Boolean,
default: true,
},
showDescription: {
type: Boolean,
default: true
}
},
}
</script>
<style lang='less'>
#list {
max-width: 500px;
.el-timeline {
padding-left: 5px;
hr {
margin-top: 4px;
margin-bottom: 4px;
}
}
.el-timeline-item {
padding-bottom: 1px;
}
.el-timeline-item__timestamp {
margin: 0px;
padding: 0px;
}
}
</style>

79
components/Logo.vue Normal file
View 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>

60
components/Nav.vue Normal file
View File

@@ -0,0 +1,60 @@
<template lang="pug">
el-menu.d-grid.nav(mode='horizontal' router background-color="#222C32")
nuxt-link(to='/login')
el-menu-item(v-if='!$auth.loggedIn' index='/login' :title="$t('common.login')")
v-icon(color='lightgreen' name='user')
el-menu-item(index='/add' :title="$t('common.add_event')")
v-icon(color='lightgreen' name='plus')
el-menu-item(v-if='$auth.loggedIn' index='/settings' :title="$t('common.settings')")
v-icon(color='orange' name='cog')
el-menu-item(v-if='$auth.user && $auth.user.is_admin' index='/admin' :title="$t('common.admin')")
v-icon(color='lightblue' name='tools')
el-menu-item(index='/export' :title="$t('common.share')")
v-icon(name='share' color='yellow')
el-menu-item(v-if='$auth.loggedIn' @click='logout' :title="$t('common.logout')")
v-icon(color='red' name='sign-out-alt')
el-popover(
placement="bottom"
trigger="click")
Search
el-menu-item(slot='reference')
v-icon(color='lightblue' name='search')
el-menu-item.float-right(index='/about' :title="$t('common.info')")
img#logo(src='/favicon.ico')
</template>
<script>
import { Message } from 'element-ui'
import Search from '@/components/Search'
export default {
name: 'Nav',
components: { Search },
methods: {
logout () {
Message({
message: this.$t('common.logout_ok'),
type: 'success'
})
this.$auth.logout()
}
}
}
</script>
<style>
.el-menu.el-menu--horizontal {
border-bottom: none;
}
</style>

7
components/README.md Normal file
View 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._

64
components/Search.vue Normal file
View File

@@ -0,0 +1,64 @@
<template lang="pug">
div.ml-2.mt-1
el-switch.mb-1(v-if='$auth.loggedIn'
active-text='solo miei'
inactive-text='tutti'
inactive-color='lightgreen'
v-model='onlyMine'
)
el-switch.mt-1.mb-1.ml-2.d-block(
inactive-text='futuri'
active-text='anche passati'
inactive-color='lightgreen'
v-model='showPast'
)
el-select.search(v-model='filter' multiple
filterable collapse-tags default-first-option
:placeholder='$t("common.search")')
el-option(v-for='(keyword, id) in keywords' :key='keyword.value'
:label='keyword.label' :value='keyword.value')
</template>
<script>
import {mapState, mapActions} from 'vuex'
export default {
data () {
return {
onlyMine: false,
withPast: true,
}
},
name :'Search',
methods: mapActions(['setSearchPlaces', 'setSearchTags', 'showPastEvents']),
computed: {
...mapState(['tags', 'places', 'filters', 'show_past_events']),
// TOFIX: optimize
keywords () {
const tags = this.tags.map( t => ({ value: 't' + t.tag, label: t.tag, weigth: t.weigth }))
const places = this.places.map( p => ({ value: 'p' + p.id, label: p.name, weigth: p.weigth }))
return tags.concat(places).sort((a, b) => b.weigth-a.weigth)
},
showPast : {
set (value) {
this.showPastEvents(value)
},
get () {
return this.show_past_events
}
},
filter: {
set (filters) {
const tags = filters.filter(f => f[0] === 't').map(t => t.slice(1))
this.setSearchTags(tags)
const places = filters.filter(f => f[0] === 'p').map(p => +p.slice(1))
this.setSearchPlaces(places)
},
get () {
return this.filters.tags.map(t => 't' + t).concat(this.filters.places.map(p => 'p' + p))
}
},
}
}
</script>

58
config.example.js Normal file
View File

@@ -0,0 +1,58 @@
/**
* GANCIO CONFIGURATION
*/
const env = process.env.NODE_ENV || 'development'
/**
* Database configuration
* `development` configuration is enabled running `yarn dev`
* while `production` with `yarn start`
* ref: http://docs.sequelizejs.com/class/lib/sequelize.js~Sequelize.html#instance-constructor-constructor
*/
const DB_CONF = {
development: {
storage: __dirname + '/db.sqlite',
dialect: 'sqlite',
},
production: {
username: '',
password: '',
database: 'gancio',
host: 'localhost',
dialect: 'postgres',
logging: false
},
}
const SECRET_CONF = {
// where events/users confirmation email are sent
admin: 'gancio@example.com',
db: DB_CONF[env],
// jwt salt secret (generate it randomly)
secret: '',
// smtp account to send email
smtp: {
host: process.env.SMTP_HOST || 'mail.example.com',
secure: true,
auth: {
user: process.env.SMTP_USER || 'gancio@example.com',
pass: process.env.SMTP_PASS || ''
}
},
}
/**
* Main Gancio configuration
*/
const SHARED_CONF = {
locale: 'it',
title: 'GANCIO',
description: 'A calendar for radical communities',
baseurl: '' || 'http://localhost:3000',
env
}
module.exports = { SHARED_CONF, SECRET_CONF, ...SECRET_CONF.db }

7
layouts/README.md Normal file
View 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).

6
layouts/default.vue Normal file
View File

@@ -0,0 +1,6 @@
<template>
<nuxt/>
</template>
<style lang="less">
@import '../assets/style.less';
</style>

3
layouts/iframe.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>
<nuxt/>
</template>

6
locales/email/en.json Normal file
View File

@@ -0,0 +1,6 @@
{
"registration_email": "registration_email",
"recover_email": "recover_email",
"press here": "press here",
"register.request": "register.request"
}

6
locales/email/es.json Normal file
View File

@@ -0,0 +1,6 @@
{
"registration_email": "registration_email",
"recover_email": "recover_email",
"press here": "press here",
"register.request": "register.request"
}

6
locales/email/it.json Normal file
View File

@@ -0,0 +1,6 @@
{
"registration_email": "registration_email",
"recover_email": "recover_email",
"press here": "press here",
"register.request": "register.request"
}

6
locales/email/zh.json Normal file
View File

@@ -0,0 +1,6 @@
{
"registration_email": "registration_email",
"recover_email": "recover_email",
"press here": "press here",
"register.request": "register.request"
}

3
locales/en.js Normal file
View File

@@ -0,0 +1,3 @@
{
"registration_email": "registration_email"
}

3
locales/es.js Normal file
View File

@@ -0,0 +1,3 @@
{
"registration_email": "registration_email"
}

139
locales/it.js Normal file
View File

@@ -0,0 +1,139 @@
const it = {
common: {
add_event: 'Nuovo evento',
next: 'Continua',
export: 'Esporta',
send: 'Invia',
where: 'Dove',
address: 'Indirizzo',
when: 'Quando',
what: 'Cosa',
media: 'Media',
login: 'Entra',
email: 'Email',
password: 'Password',
register: 'Registrati',
description: 'Descrizione',
remove: 'Elimina',
hide: 'Nascondi',
search: 'Cerca',
edit: 'Modifica',
info: 'Info',
confirm: 'Conferma',
admin: 'Amministra',
users: 'Utenti',
events: 'Eventi',
places: 'Luoghi',
settings: 'Opzioni',
actions: 'Azioni',
deactivate: 'Disattiva',
remove_admin: 'Rimuovi Admin',
activate: 'Attiva',
save: 'Salva',
preview: 'Anteprima',
logout: 'Esci',
share: 'Esporta',
name: 'Nome',
associate: 'Associa',
edit_event: 'Modifica evento',
related: 'Memoria storica',
add: 'Aggiungi',
logout_ok: 'Uscita correttamente',
copy: 'Copia',
recover_password: 'Recupera password',
new_password: 'Nuova password'
},
login: {
description: `Entrando puoi pubblicare nuovi eventi.`,
check_email: 'Controlla la tua posta (anche lo spam)',
not_registered: 'Non sei registrata?',
forgot_password: 'Dimenticato la password?',
error: 'Errore: ',
insert_email: 'Inserisci la mail',
ok: 'Tutto rego'
},
export: {
intro: `Contrariamente alle piattaforme del capitalismo, che fanno di tutto per tenere
i dati e gli utenti al loro interno, crediamo che le informazioni, come le persone,
debbano essere libere. Per questo puoi rimanere aggiornata sugli eventi che vuoi, come meglio credi, senza necessariamente passare da questo sito.`,
email_description: `Puoi ricevere via mail gli eventi che ti interessano.`,
insert_your_address: 'Indirizzo email',
feed_description: `Per seguire gli aggiornamenti da computer o smartphone senza la necessità di aprire periodicamente il sito, il metodo consigliato è quello dei Feed RSS.</p>
<p>Con i feed rss utilizzi un'apposita applicazione per ricevere aggiornamenti dai siti che più ti interessano. È un buon metodo per seguire anche molti siti in modo molto rapido, senza necessità di creare un account o altre complicazioni.</p>
<li>Se hai Android, ti consigliamo <a href="https://play.google.com/store/apps/details?id=net.frju.flym">Flym</a> o Feeder</li>
<li>Per iPhone/iPad puoi usare <a href="https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8">Feed4U</a></li>
<li>Per il computer fisso/portatile consigliamo Feedbro, da installare all'interno <a href="https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/">di Firefox </a>o <a href="https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa">di Chrome</a> e compatibile con tutti i principali sistemi operativi.</li>
<br/>
Aggiungendo questo link al tuo lettore di feed, rimarrai aggiornata.`,
ical_description: `I computer e gli smartphone sono comunemente attrezzati con un'applicazione per gestire un calendario. A questi programmi solitamente è possibile far importare un calendario remoto.`,
list_description: `Se hai un sito web e vuoi mostrare una lista di eventi, puoi usare il seguente codice`
},
register: {
description: `I movimenti hanno bisogno di organizzarsi e autofinanziarsi. <br/>Questo è un dono per voi, usatelo solo per eventi non commerciali e ovviamente antifascisti, antisessisti, antirazzisti.
<br/>Prima di poter pubblicare <strong>dobbiamo approvare l'account</strong>, considera che <strong>dietro questo sito ci sono delle persone</strong> di
carne e sangue, scrivici quindi due righe per farci capire che eventi vorresti pubblicare.`,
error: 'Errore: ',
admin_complete: 'Sei il primo utente e quindi sei amministratore!',
complete: 'Confermeremo la registrazione quanto prima.',
request: 'Richiesta di registrazione',
registration_email: `Ciao,
ci è arrivata una richiesta di registrazione su gancio, la confermeremo quanto prima.`
},
event: {
anon: 'Anonimo',
anon_description: `Puoi inserire un evento senza registrarti o fare il login,
ma in questo caso dovrai aspettare che qualcuno lo legga confermando che si
tratta di un evento adatto a questo spazio, delegando questa scelta. Inoltre non sarà possibile modificarlo.<br/><br/>
Puoi invece fare il <a href='/login'>login</a> o <a href='/registrarti'>registrarti</a>,
altrimenti vai avanti e riceverai una risposta il prima possibile. `,
multidate_description: 'tanti giorni',
date_description: `Quand'è il gancio?`,
dates_description: 'Che giorni?',
same_day: 'stesso giorno',
what_description: 'Nome evento',
description_description: 'Descrizione, dajene di copia/incolla',
tag_description: 'Tag...',
media_description: 'Puoi aggiungere un volantino',
time_start_description: 'Comincia alle',
time_end_description: 'Se vuoi puoi specificare un orario di fine.',
added: 'Evento aggiunto',
added_anon: 'Evento aggiunto, verrà confermato quanto prima.',
where_description: `Dov'è il gancio? Se il posto non è presente, scrivilo e <b>premi invio</b>. `,
confirmed: 'Evento confermato'
},
admin: {
mastodon_instance: 'Istanza',
mastodon_description: 'Puoi associare un account mastodon a questa istanza di gancio, ogni evento verrà pubblicato lì.',
place_description: `Nel caso in cui un luogo sia errato o cambi indirizzo, puoi modificarlo. <br/>Considera che tutti gli eventi associati a questo luogo cambieranno indirizzo (anche quelli passati!)`,
event_confirm_description: 'Puoi confermare qui gli eventi inseriti da utenti anonimi'
},
auth: {
not_confirmed: 'Non abbiamo ancora confermato questa mail...',
fail: 'Autenticazione fallita. Sicura la password è giusta? E la mail?'
},
settings: {
change_password: 'Cambia password'
},
err: {
register_error: 'Errore nella registrazione'
},
firstrun: {
basic: `Inserisci titolo e descrizione della tua istanza di gancio.`,
database: `Gancio ha bisogno di un database postgresql!`,
smtp: `Inserisci un account SMTP relativo a questa istanza di gancio.`
}
}
export default it

8
middleware/README.md Normal file
View 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).

91
nuxt.config.js Normal file
View File

@@ -0,0 +1,91 @@
const argv = require('yargs').argv
const path = require('path')
const config_path = path.resolve(argv.config || './config.js')
const config = require(config_path).SHARED_CONF
module.exports = {
mode: 'universal',
/*
** Headers of the page
*/
head: {
title: config.title,
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: config.description }
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
},
dev: (process.env.NODE_ENV !== 'production'),
serverMiddleware: [
{ path: '/api', handler: '@/server/api/index.js' }
],
/*
** Customize the progress-bar color
*/
// loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
'bootstrap/dist/css/bootstrap.css',
'element-ui/lib/theme-chalk/index.css'
],
env: {
config
},
/*
** Plugins to load before mounting the App
*/
plugins: [
'@/plugins/element-ui', // UI library -> https://element.eleme.io/#/en-US/
'@/plugins/filters', // text filters, datetime, etc.
'@/plugins/i18n', // localization plugin
'@/plugins/vue-awesome', // icon
{ src: '@/plugins/v-calendar', ssr: false } // calendar, TO-REDO
],
/*
** Nuxt.js modules
*/
modules: [
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios',
'@nuxtjs/auth'
],
/*
** Axios module configuration
*/
axios: {
baseURL: config.baseurl + '/api',
browserBaseURL: config.baseurl + '/api',
prefix: '/api'
// credentials: true
// See https://github.com/nuxt-community/axios-module#options
},
auth: {
strategies: {
local: {
endpoints: {
login: { url: '/auth/login', method: 'post', propertyName: 'token' },
logout: false,
user: { url: '/auth/user', method: 'get', propertyName: false }
}
}
}
},
/*
** Build configuration
*/
build: {
transpile: [/^element-ui/, /^vue-awesome/],
splitChunks: {
layouts: true
}
}
}

76
package.json Normal file
View File

@@ -0,0 +1,76 @@
{
"name": "gancio",
"version": "0.9.3",
"description": "My well-made Nuxt.js project",
"author": "lesion",
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
"build": "nuxt build",
"start": "cross-env NODE_ENV=production node server/index.js",
"generate": "nuxt generate",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"precommit": "npm run lint",
"migrate:dev": "sequelize db:migrate",
"migrate": "NODE_ENV=production sequelize db:migrate"
},
"bin": {
"gancio": "server/index.js"
},
"files": [
"server/*",
"nuxt.config.js",
"dist/*"
],
"dependencies": {
"@nuxtjs/auth": "^4.6.5",
"@nuxtjs/axios": "^5.5.3",
"axios": "^0.19.0",
"bcrypt": "^3.0.5",
"body-parser": "^1.18.3",
"bootstrap": "4.3.1",
"cookie-parser": "^1.4.4",
"cors": "^2.8.5",
"cross-env": "^5.2.0",
"dayjs": "^1.8.14",
"element-ui": "^2.9.1",
"email-templates": "^5.1.0",
"express": "^4.17.1",
"express-jwt": "^5.3.1",
"ics": "^2.13.2",
"jsonwebtoken": "^8.5.1",
"less": "^3.9.0",
"mastodon-api": "^1.3.0",
"morgan": "^1.9.1",
"multer": "^1.4.1",
"nuxt": "^2.8.1",
"pg": "^7.11.0",
"sequelize": "^5.8.7",
"sequelize-cli": "^5.4.0",
"sharp": "^0.22.0",
"sqlite3": "^4.0.8",
"v-calendar": "^1.0.0-beta.14",
"vue-awesome": "^3.5.3",
"vue-i18n": "^8.10.0"
},
"devDependencies": {
"@nuxtjs/eslint-config": "^0.0.1",
"babel-eslint": "^10.0.1",
"eslint": "^5.15.1",
"eslint-config-prettier": "^4.3.0",
"eslint-config-standard": ">=12.0.0",
"eslint-loader": "^2.1.2",
"eslint-plugin-import": ">=2.17.3",
"eslint-plugin-jest": ">=22.6.4",
"eslint-plugin-node": ">=9.1.0",
"eslint-plugin-nuxt": ">=0.4.2",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-promise": ">=4.0.1",
"eslint-plugin-standard": ">=4.0.0",
"eslint-plugin-vue": "^5.2.2",
"less-loader": "^5.0.0",
"nodemon": "^1.19.1",
"prettier": "^1.17.1",
"pug-plain-loader": "^1.0.0",
"webpack-cli": "^3.3.2"
}
}

6
pages/README.md Normal file
View 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).

50
pages/about.vue Normal file
View File

@@ -0,0 +1,50 @@
<template lang="pug">
el-card
nuxt-link.float-right(to='/')
v-icon(name='times' color='red')
h3 {{$t('common.info')}}
p.
Gancio e' un progetto dell'<a href='https://autistici.org/underscore'>underscore hacklab</a> e uno dei
servizi di <a href='https://cisti.org'>cisti.org</a>.
h5 Cos'è gancio?
p.
Uno strumento di condivisione di eventi per comunità radicali.
Dentro gancio puoi trovare e inserire eventi.
Gancio, come tutto <a href='https://cisti.org'>cisti.org</a> è uno strumento
antisessista, antirazzista, antifascista e anticapitalista, riflettici quando
pubblichi un evento.
h5 Ok, ma cosa vuol dire gancio?
blockquote.
Se vieni a Torino e dici: "ehi, ci diamo un gancio alle 8?" nessuno si presenterà con i guantoni per fare a mazzate.
Darsi un gancio vuol dire beccarsi alle ore X in un posto Y
li A: a che ora è il gancio in radio per andare al presidio?
li B: non so ma domani non posso venire, ho gia' un gancio per caricare il bar.
br
h5 Contatti
p.
Hai scritto una nuova interfaccia per gancio? Vuoi aprire un nuovo nodo di gancio nella tua città?
C'è qualcosa che vorresti migliorare? Per contribuire i sorgenti sono liberi e disponibili
<a href='https://git.lattuga.net/cisti/gancio'>qui</a>. Aiuti e suggerimenti sono sempre benvenuti, puoi scriverci
su underscore chicciola autistici.org
</template>
<script>
export default {
data () {
return {
open: true,
}
},
methods: {
close (done) {
this.$router.replace('/')
done()
}
}
}
</script>

283
pages/add/_edit.vue Normal file
View File

@@ -0,0 +1,283 @@
<template lang="pug">
el-card
nuxt-link.float-right(to='/')
v-icon(name='times' color='red')
h5 {{edit?$t('common.edit_event'):$t('common.add_event')}}
el-form(v-loading='loading')
el-tabs.mb-2(v-model='activeTab')
//- NOT LOGGED EVENT
el-tab-pane(v-if='!$auth.loggedIn')
span(slot='label') {{$t('event.anon')}} <v-icon name='user-secret'/>
p(v-html="$t('event.anon_description')")
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('common.next')}}
//- WHERE
el-tab-pane
span(slot='label') <v-icon name='map-marker-alt'/> {{$t('common.where')}}
p(v-html="$t('event.where_description')")
el-select.mb-3(v-model='event.place.name'
@change='placeChoosed'
filterable allow-create
default-first-option
)
el-option(v-for='place in places' :label='place.name' :value='place.name' :key='place.id')
span {{place.name}}
div {{$t("common.address")}}
el-input.mb-3(ref='address' v-model='event.place.address'
:disabled='places_name.indexOf(event.place.name)>-1'
@keydown.native.enter='next')
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('common.next')}}
//- WHEN
el-tab-pane
span(slot='label') {{$t('common.when')}} <v-icon name='clock'/>
span {{event.multidate ? $t('event.dates_description') : $t('event.date_description')}}
el-switch.float-right(v-model='event.multidate' :active-text="$t('event.multidate_description')")
v-date-picker.mb-3(
:mode='event.multidate ? "range" : "single"'
:attributes='attributes'
v-model='date'
is-inline
is-expanded
:min-date='new Date()'
)
el-row
el-col(:span='12')
div {{$t('event.time_start_description')}}
el-time-select.mb-3(ref='time_start'
v-model="time.start"
:picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
div {{$t('event.time_end_description')}}
el-time-select(v-model='time.end'
:picker-options="{start: '00:00', step: '00:30', end: '24:00'}")
el-col(:span='12')
List(:events='todayEvents' :title='$t("event.same_day")')
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('common.next')}}
//- WHAT
el-tab-pane
span(slot='label') {{$t('common.what')}} <v-icon name='file-alt'/>
span {{$t('event.what_description')}}
el-input.mb-3(v-model='event.title' ref='title')
span {{$t('event.description_description')}}
el-input.mb-3(v-model='event.description' type='textarea' :rows='9')
span {{$t('event.tag_description')}}
br
el-select(v-model='event.tags' multiple filterable allow-create
default-first-option placeholder='Tag')
el-option(v-for='tag in tags' :key='tag.tag'
:label='tag' :value='tag')
el-button.float-right(@click.native='next' :disabled='!couldProceed') {{$t('common.next')}}
el-tab-pane
span(slot='label') {{$t('common.media')}} <v-icon name='image'/>
el-upload.text-center(
action=''
:limit="1"
:auto-upload='false'
drag
:on-change='uploadedFile'
:multiple='false'
:file-list="fileList"
)
i.el-icon-upload
div.el-upload__text {{$t('event.media_description')}}
el-button.float-right(@click='done' :disabled='!couldProceed') {{edit?$t('common.edit'):$t('common.send')}}
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex'
import moment from 'dayjs'
import List from '@/components/List'
import { Message } from 'element-ui'
export default {
name: 'Add',
components: { List },
data() {
return {
event: {
place: { name: '', address: '' },
title: '', description: '', tags: [],
multidate: false,
image: false
},
fileList: [],
open: true,
id: null,
activeTab: "0",
date: null,
time: { start: '20:00', end: null },
edit: false,
loading: true,
}
},
name: 'newEvent',
watch: {
'time.start' (value) {
let [h, m] = value.split(':')
this.time.end = (Number(h)+1) + ':' + m
}
},
async mounted () {
if (this.$route.params.edit) {
this.id = this.$route.params.edit
this.edit = true
const event = await this.$axios.$get('/event/'+ this.id)
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()
this.loading = false
},
computed: {
...mapState({
tags: state => state.tags.map(t => t.tag ),
places_name: state => state.places.map(p => p.name ).sort((a, b) => b.weigth-a.weigth),
places: state => state.places,
user: state => state.user,
events: state => state.events
}),
todayEvents () {
const date = moment(this.date)
return this.events.filter(e => date.isSame(moment(e.start_datetime), 'day'))
},
...mapGetters(['filteredEvents']),
attributes () {
return this.events.filter(e => !e.past).map(this.eventToAttribute)
},
disableAddress () {
return this.places_name.find(p => p.name === this.event.place.name)
},
couldProceed () {
const t = this.$auth.loggedIn ? -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
case 3+t:
return this.event.title.length>0
case 4+t:
return this.event.place.name.length>0 &&
this.event.place.address.length>0 &&
(this.date && this.time.start) &&
this.event.title.length>0
}
}
},
methods: {
...mapActions(['addEvent', 'updateEvent', 'updateMeta']),
eventToAttribute(event) {
let e = {
key: event.id,
customData: event,
order: event.start_datetime,
}
const day = moment(event.start_datetime).date()
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
},
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()
},
uploadedFile(file, fileList) {
this.event.image = file
},
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.raw, 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.loading = true
try {
if (this.edit) {
await this.updateEvent(formData)
} else {
await this.addEvent(formData)
}
this.updateMeta()
this.loading = false
this.$router.replace('/')
Message({ type: 'success', message: this.$auth.loggedIn ? this.$t('event.added') : this.$t('event.added_anon')})
} catch (e) {
this.loading = false
console.error(e)
}
}
}
}
</script>

234
pages/admin.vue Normal file
View File

@@ -0,0 +1,234 @@
<template lang="pug">
el-card(:title='$t("common.admin")' width='80%' :visible='open' :before-close='close')
nuxt-link.float-right(to='/')
v-icon(name='times' color='red')
h5 {{$t('common.admin')}}
el-tabs(tabPosition='left' v-model='tab')
//- USERS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='users')
span.ml-1 {{$t('common.users')}}
el-table(:data='paginatedUsers' small)
el-table-column(label='Email')
template(slot-scope='data')
el-popover(trigger='hover' :content='data.row.description' width='400')
span(slot='reference') {{data.row.email}}
el-table-column(:label="$t('common.actions')")
template(slot-scope='data')
el-button.mr-1(size='mini'
:type='data.row.is_active?"warning":"success"'
@click='toggle(data.row)') {{data.row.is_active?$t('common.deactivate'):$t('common.activate')}}
el-button(size='mini'
:type='data.row.is_admin?"danger":"warning"'
@click='toggleAdmin(data.row)') {{data.row.is_admin?$t('common.remove_admin'):$t('common.admin')}}
el-pagination(:page-size='perPage' :currentPage.sync='userPage' :total='users.length')
//- PLACES
el-tab-pane.pt-1
template(slot='label')
v-icon(name='map-marker-alt')
span.ml-1 {{$t('common.places')}}
p(v-html="$t('admin.place_description')")
el-form.mb-2(:inline='true' label-width='120px')
el-form-item(:label="$t('common.name')")
el-input.mr-1(:placeholder='$t("common.name")' v-model='place.name')
el-form-item(:label="$t('common.address')")
el-input.mr-1(:placeholder='$t("common.address")' v-model='place.address')
el-button(variant='primary' @click='savePlace') {{$t('common.save')}}
el-table(:data='paginatedPlaces' small @current-change="val => place=val")
el-table-column(:label="$t('common.name')")
template(slot-scope='data') {{data.row.name}}
el-table-column(:label="$t('common.address')")
template(slot-scope='data') {{data.row.address}}
el-pagination(:page-size='perPage' :currentPage.sync='placePage' :total='places.length')
//- EVENTS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='calendar')
span.ml-1 {{$t('common.events')}}
p {{$t('admin.event_confirm_description')}}
el-table(:data='paginatedEvents' small primary-key='id' v-loading='loading')
el-table-column(:label='$t("common.name")')
template(slot-scope='data') {{data.row.title}}
el-table-column(:label='$t("common.where")')
template(slot-scope='data') {{data.row.place.name}}
el-table-column(:label='$t("common.confirm")')
template(slot-scope='data')
el-button(type='primary' @click='confirm(data.row.id)' size='mini') {{$t('common.confirm')}}
el-button(type='success' @click='preview(data.row.id)' size='mini') {{$t('common.preview')}}
el-pagination(:page-size='perPage' :currentPage.sync='eventPage' :total='events.length')
//- TAGS
//- el-tab-pane.pt-1
//- template(slot='label')
//- v-icon(name='tags')
//- span {{$t('common.tags')}}
//- p {{$t('admin.tag_description')}}
//- el-tag(v-if='tag.tag' :color='tag.color' size='mini') {{tag.tag}}
//- el-form(:inline='true' label-width='120px')
//- el-form-item(:label="$t('common.color')")
//- el-color-picker(v-model='tag.color' @change='updateColor')
//- el-table(:data='paginatedTags' striped small hover
//- highlight-current-row @current-change="tagSelected")
//- el-table-column(:label="$t('common.tag')")
//- template(slot-scope='data')
//- el-tag(:color='data.row.color' size='mini') {{data.row.tag}}
//- el-pagination(:page-size='perPage' :currentPage.sync='tagPage' :total='tags.length')
//- SETTINGS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='cog')
span {{$t('common.settings')}}
//- el-form(inline @submit.prevent.stop='save_settings' label-width='140px')
//- p {{$t('settings.name_description')}}
//- el-form-item(:label="$t('settings.name')")
//- el-input(v-model="settings.title")
//- el-form-item(:label="$t('settings.description')")
//- el-input(v-model="settings.description")
//- el-button(slot='append' @click='associate' :disabled='!mastodon_instance.length') {{$t('common.associate')}}
el-form(inline @submit.prevent.stop='associatemastodon_instance')
span {{$t('admin.mastodon_description')}}
el-input(v-model="settings.mastodon_instance")
span(slot='prepend') {{$t('admin.mastodon_instance')}}
el-button(slot='append' @click='associate' :disabled='!mastodon_instance.length') {{$t('common.associate')}}
</template>
<script>
import { mapState } from 'vuex'
import { Message } from 'element-ui'
export default {
name: 'Admin',
middleware: ['auth'],
data () {
return {
perPage: 10,
users: [],
userFields: ['email', 'action'],
placeFields: ['name', 'address'],
placePage: 1,
userPage: 1,
eventPage: 1,
tagPage: 1,
tagFields: ['tag', 'color'],
description: '',
place: {name: '', address: '' },
tag: {name: '', color: ''},
events: [],
loading: false,
settings: {
mastodon_instance: '',
},
settings: {},
tab: "0",
open: true
}
},
async mounted () {
const code = this.$route.query.code
if (code) {
this.tab = "4"
const instance = await this.$axios.$post('/user/code', {code, is_admin: true})
}
},
async asyncData ({ $axios, params, store }) {
try {
const users = await $axios.$get('/users')
const events = await $axios.$get('/event/unconfirmed')
const settings = await $axios.$get('/settings')
return { users, events, settings, mastodon_instance: settings && settings.mastodon_auth && settings.mastodon_auth.instance || ''}
} catch ( e ) {
console.error(e)
}
},
computed: {
...mapState(['tags', 'places']),
paginatedEvents () {
return this.events.slice((this.eventPage-1) * this.perPage,
this.eventPage * this.perPage)
},
paginatedTags () {
return this.tags.slice((this.tagPage-1) * this.perPage,
this.tagPage * this.perPage)
},
paginatedUsers () {
return this.users.slice((this.userPage-1) * this.perPage,
this.userPage * this.perPage)
},
paginatedPlaces () {
return this.places.slice((this.placePage-1) * this.perPage,
this.placePage * this.perPage)
},
},
methods: {
placeSelected (items) {
if (items.length === 0 ) {
this.place.name = this.place.address = ''
return
}
const item = items[0]
this.place.name = item.name
this.place.address = item.address
this.place.id = item.id
},
tagSelected (tag) {
this.tag = { color: tag.color, tag: tag.tag }
},
async savePlace () {
// const place = await api.updatePlace(this.place)
},
async toggle(user) {
user.is_active = !user.is_active
// const newuser = await api.updateUser(user)
},
async toggleAdmin(user) {
user.is_admin = !user.is_admin
// const newuser = await api.updateUser(user)
},
async updateColor () {
// try {
// const newTag = await this.$axios.$put('/tag', this.tag)
// } catch (e) {
// console.log(e)
// }
},
preview (id) {
this.$router.push(`/event/${id}`)
},
async associate () {
if (!this.mastodon_instance) return
const url = await this.$axios.$post('/settings/getauthurl', {instance: this.mastodon_instance})
setTimeout( () => window.location.href=url, 100);
},
async confirm (id) {
try {
this.loading = true
await this.$axios.$get(`/event/confirm/${id}`)
this.loading = false
Message({
message: this.$t('event.confirmed'),
type: 'success'
})
this.events = this.events.filter(e => e.id !== id)
} catch (e) {
}
},
close (done) {
console.log('oppure qui !')
this.$router.replace('/')
// done()
}
}
}
</script>

29
pages/embed/list.vue Normal file
View File

@@ -0,0 +1,29 @@
<template lang="pug">
List(:events="events" :title='title')
</template>
<script>
import { mapState } from 'vuex'
import List from '../../components/List'
import moment from 'dayjs'
export default {
layout: 'iframe',
components: { List },
computed: mapState(['config']),
async asyncData ({ $axios, req, res }) {
const title = req && req.query && req.query.title || this.config.title
const tags = req && req.query && req.query.tags
const places = req && req.query && req.query.places
const now = new Date()
let params = []
if (places) params.push(`places=${places}`)
if (tags) params.push(`tags=${tags}`)
params = params.length ? `?${params.join('&')}` : ''
const events = await $axios.$get(`/export/json${params}`)
return { events, title }
},
}
</script>

201
pages/event/_id.vue Normal file
View File

@@ -0,0 +1,201 @@
<template lang="pug">
el-card#eventDetail
//- close button
nuxt-link.float-right(to='/')
el-button(circle icon='el-icon-close' type='danger' size='small' plain)
//- title, where, when
h5.text-center {{event.title}}
div.nextprev
nuxt-link(v-if='prev' :to='`/event/${prev.id}`')
el-button(icon='el-icon-arrow-left' round type='success')
nuxt-link.float-right(v-if='next' :to='`/event/${next.id}`')
el-button(icon='el-icon-arrow-right' round type='success')
//- image
img(:src='imgPath' v-if='event.image_path')
.info
div {{event|event_when}}
div {{event.place.name}} - {{event.place.address}}
//- description and tags
div(v-if='event.description || event.tags')
pre(v-html='event.description')
el-tag.mr-1(v-for='tag in event.tags'
size='mini' :key='tag.tag') {{tag.tag}}
//- show hide, confirm, delete, edit buttons when allowed
div(v-if='mine')
hr
el-button(v-if='event.is_visible' size='mini' plain type='warning' @click.prevents='toggle' icon='el-icon-view') {{$t('common.hide')}}
el-button(v-else plain type='success' size='mini' @click.prevents='toggle' icon='el-icon-view') {{$t('common.confirm')}}
el-button(plain type='danger' size='mini' @click.prevent='remove' icon='el-icon-remove') {{$t('common.remove')}}
el-button(plain type='primary' size='mini' @click='$router.replace(`/add/${event.id}`)' icon='el-icon-edit') {{$t('common.edit')}}
//- comments
.card-body(v-if='event.activitypub_id')
strong {{$t('common.related')}} -
a(:href='`https://mastodon.cisti.org/web/statuses/${event.activitypub_id}`') {{$t('common.add')}}
.card-header(v-for='comment in event.comments' :key='comment.id')
img.avatar(:src='comment.data.last_status.account.avatar')
strong {{comment.author}}
a.float-right(:href='comment.data.last_status.url')
small {{comment.data.last_status.created_at|datetime}}
div.mt-1(v-html='comment_filter(comment.text)')
img(v-for='img in comment.data.last_status.media_attachments' :src='img.preview_url')
</template>
<script>
import { mapState, mapActions, mapGetters } from 'vuex'
export default {
name: 'Event',
// transition: null,
// Watch for $route.query.page to call Component methods (asyncData, fetch, validate, layout, etc.)
// watchQuery: ['id'],
// Key for <NuxtChild> (transitions)
// key: to => to.fullPath,
// Called to know which transition to apply
// transition(to, from) {
// console.log('dentro transition')
// if (!from) return 'slide-left'
// return +to.params.id < +from.params.id ? 'slide-right' : 'slide-left'
// },
head () {
return {
title: this.event.title,
meta: [
// hid is used as unique identifier. Do not use `vmid` for it as it will not work
// { hid: 'description', name: 'description', content: this.event.description },
// { hid: 'og-description', name: 'og:description', content: this.event.description },
{ hid: 'og-title', property: 'og:title', content: this.event.title },
{ hid: 'og-url', property: 'og:url', content: `event/${this.event.id}` },
{ property: 'og:type', content: 'event'},
{ property: 'og:image', content: this.imgPath }
]
}
},
computed: {
...mapGetters(['filteredEvents']),
next () {
let found = false
return this.filteredEvents.find(e => {
if (found) return e
if (e.id === this.event.id) found = true
})
},
prev () {
let prev = false
this.filteredEvents.find(e => {
if (e.id === this.event.id) return true
prev = e
})
return prev
},
imgPath () {
return this.event.image_path && '/media/' + this.event.image_path
},
mine () {
if (!this.$auth.user) return false
return this.event.userId === this.$auth.user.id || this.$auth.user.is_admin
},
},
async asyncData ( { $axios, params }) {
const event = await $axios.$get(`/event/${params.id}`)
return { event, id: params.id}
},
methods: {
...mapActions(['delEvent']),
keydown (e) {
console.error(e)
},
comment_filter (value) {
return value.replace(/<a.*href="([^">]+).*>(?:.(?!\<\/a\>))*.<\/a>/, (orig, url) => {
// get extension
const ext = url.slice(-4)
if (['.mp3', '.ogg'].indexOf(ext)>-1) {
return `<audio controls><source src='${url}'></audio>`
} else {
return orig
}
})
},
async remove () {
try {
await this.$axios.delete(`/user/event/${this.id}`)
this.delEvent(Number(this.id))
this.$router.back()
} catch (e) {
console.error(e)
}
},
async toggle () {
try {
if (this.event.is_visible) {
await this.$axios.$get(`/event/unconfirm/${this.id}`)
this.event.is_visible = false
} else {
await this.$axios.$get(`/event/confirm/${this.id}`)
this.event.is_visible = true
}
} catch (e) {
console.error(e)
}
}
}
}
</script>
<style lang='less'>
#eventDetail {
max-width: 1000px;
border-radius: 0px;
margin: 0 auto;
pre {
color: #404246;
font-size: 1em;
font-family: BlinkMacSystemFont,-apple-system,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif !important;
}
h5 {
font-size: 1.4em;
min-height: 40px;
}
.info {
margin: 10px;
font-size: 1.3em;
font-weight: 600;
text-align: center;
}
img {
width: 100%;
max-height: 89vh;
object-fit: contain;
}
.avatar {
width: auto;
height: 40px;
border-radius: 5px;
}
.nextprev {
font-size: 10px;
margin-bottom: 5px;
}
}
@media only screen and (max-width: 768px) {
#eventDetail {
font-size: 12px;
}
}
</style>

142
pages/export.vue Normal file
View File

@@ -0,0 +1,142 @@
<template lang="pug">
el-card
nuxt-link.float-right(to='/')
v-icon(name='times' color='red')
h5 {{$t('common.export')}}
p {{$t('export.intro')}}
Search
//- li(v-if='filters.tags.length') {{$t('common.tags')}}:
//- el-tag.ml-1(size='mini' v-for='tag in filters.tags' :key='tag.tag') {{tag}}
//- li(v-if='filters.places.length') {{$t('common.places')}}:
//- el-tag.ml-1(size='mini' v-for='place in filters.places' :key='place.id') {{place}}
el-tabs.mt-2(v-model='type')
el-tab-pane.pt-1(label='email' name='email')
p(v-html='$t(`export.email_description`)')
el-form(@submit.native.prevent)
//- el-switch(v-model='notification.notify_on_add' :active-text="$t('notify_on_insert')")
//- br
//- el-switch.mt-2(v-model='notification.send_notification' :active-text="$t('send_notification')")
el-input.mt-2(v-model='notification.email' :placeholder="$t('export.insert_your_address')" ref='email')
el-button.mt-2.float-right(native-type= 'submit' type='success' @click='add_notification') {{$t('common.send')}}
el-tab-pane.pt-1(label='feed rss' name='feed')
span(v-html='$t(`export.feed_description`)')
el-input(v-model='link')
el-button(slot='append' plain type="primary" icon='el-icon-document' ) {{$t("common.copy")}}
el-tab-pane.pt-1(label='ics/ical' name='ics')
p(v-html='$t(`export.ical_description`)')
el-input(v-model='link')
el-button(slot='append' plain type="primary" icon='el-icon-document') {{$t("common.copy")}}
el-tab-pane.pt-1(label='list' name='list')
p(v-html='$t(`export.list_description`)')
el-row
el-col.mr-2(:span='11')
el-input(v-model='list.title') Title
el-col.float-right(:span='12')
List(
:title='list.title'
:events='filteredEvents'
)
el-input.mb-1(type='textarea' v-model='listScript' readonly )
el-button.float-right(plain type="primary" icon='el-icon-document') {{$t('common.copy')}}
//- el-tab-pane.pt-1(label='calendar' name='calendar')
//- p(v-html='$t(`export.calendar_description`)')
//- //- no-ssr
//- Calendar.mb-1
//- el-input.mb-1(type='textarea' v-model='script')
//- el-button.float-right(plain type="primary" icon='el-icon-document') Copy
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import path from 'path'
import Calendar from '@/components/Calendar'
import List from '@/components/List'
import Search from '@/components/Search'
import {intersection} from 'lodash'
import { Message } from 'element-ui'
export default {
name: 'Export',
components: { List, Search },
data () {
return {
type: 'email',
notification: { email: '' },
list: { title: 'Gancio' },
}
},
// filters,
methods: {
async add_notification () {
if (!this.notification.email){
Message({message:'Inserisci una mail', type: 'error'})
// return this.$refs.email.focus()
}
// await api.addNotification({ ...this.notification, filters: this.filters})
// this.$refs.modal.hide()
Message({message: this.$t('email_notification_activated'), type: 'success'})
},
imgPath (event) {
return event.image_path && event.image_path
},
close (done) {
this.$router.replace('/')
done()
}
},
computed: {
...mapState(['filters', 'events']),
...mapGetters(['filteredEvents']),
listScript () {
const params = []
if (this.list.title) {
params.push(`title=${this.list.title}`)
}
if (this.filters.places) {
params.push(`places=${this.filters.places}`)
}
return `<iframe src="/embed/list?${params.join('&')}"></iframe>`
},
link () {
const tags = this.filters.tags.join(',')
const places = this.filters.places.join(',')
let query = ''
if (tags || places) {
query = '?'
if (tags) {
query += 'tags=' + tags
if (places) { query += '&places=' + places }
} else {
query += 'places=' + places
}
}
return `/api/export/${this.type}${query}`
},
showLink () {
return (['feed', 'ics'].indexOf(this.type)>-1)
},
}
}
</script>
<style>
#list {
max-height: 400px;
overflow-y: scroll;
}
</style>

29
pages/index.vue Normal file
View File

@@ -0,0 +1,29 @@
<template lang="pug">
#home
Nav
Home
</template>
<script>
import Home from '~/components/Home.vue'
import Nav from '~/components/Nav.vue'
import { mapState } from 'vuex'
export default {
name: 'Index',
async fetch ({ store, $axios }) {
try {
const now = new Date()
const events = await $axios.$get(`/event/${now.getMonth()}/${now.getFullYear()}`)
store.commit('setEvents', events)
const { tags, places } = await $axios.$get('/event/meta')
store.commit('update', { tags, places })
} catch(e) {
console.error(e)
}
},
computed: mapState(['events']),
components: { Nav, Home },
}
</script>

83
pages/login.vue Normal file
View File

@@ -0,0 +1,83 @@
<template lang='pug'>
el-card
nuxt-link.float-right(to='/')
v-icon(name='times' color='red')
h5 {{$t('common.login')}}
el-form(v-loading='loading' method='POST' action='/api/auth/login')
p(v-html="$t('login.description')")
el-input.mb-2(v-model='email' type='email' name='email'
:placeholder='$t("common.email")' autocomplete='email' ref='email')
v-icon(name='user' slot='prepend')
el-input.mb-1(v-model='password' @keyup.enter.native="submit" name='password'
type='password' :placeholder='$t("common.password")')
v-icon(name='lock' slot='prepend')
el-button.mr-1(plain type="success" native-type='submit'
:disabled='disabled' @click='submit') {{$t('common.login')}}
nuxt-link(to='/register')
el-button.mt-1(plain type="primary") {{$t('login.not_registered')}}
a.float-right(href='#' @click='forgot') {{$t('login.forgot_password')}}
</template>
<script>
import { mapActions } from 'vuex'
import { Message } from 'element-ui'
import get from 'lodash/get'
export default {
name: 'Login',
data () {
return {
open: true,
password: '',
email: '',
loading: false
}
},
computed: {
disabled () {
if (process.server) return false
return !this.email || !this.password
}
},
methods: {
close () {
this.$router.replace('/')
},
...mapActions(['login']),
async forgot () {
if (!this.email) {
Message({ message: this.$t('login.insert_email'), type: 'error' })
this.$refs.email.focus()
return
}
this.loading = true
await this.$axios.$post('/user/recover', { email: this.email })
this.loading = false
Message({ message: this.$t('login.check_email'), type: 'success' })
},
async submit (e) {
e.preventDefault()
try {
this.loading = true
await this.$auth.loginWith('local', { data: { email: this.email, password: this.password } })
this.loading = false
Message({ message: this.$t('login.ok'), type: 'success' })
} catch (e) {
e = get(e, 'response.data.message', e)
Message({ message: this.$t('login.error') + this.$t(e), type: 'error' })
this.loading = false
return
}
this.email = this.password = ''
}
}
}
</script>

54
pages/recover/_code.vue Normal file
View File

@@ -0,0 +1,54 @@
<template lang="pug">
el-card
nuxt-link.float-right(to='/')
el-button(circle icon='el-icon-close' type='danger' size='small' plain)
h5 <img src='/favicon.ico'/> {{$t('common.recover_password')}}
div(v-if='valid')
el-form
el-form-item {{$t('common.new_password')}}
el-input(type='password', v-model='new_password')
el-button(plain type="success" icon='el-icon-send', @click='change_password') {{$t('common.send')}}
div(v-else) {{$t('recover.not_valid_code')}}
</template>
<script>
import { Message } from 'element-ui'
export default {
name: 'Recover',
data () {
return { new_password: '' }
},
async asyncData({ params, $axios }) {
const code = params.code
try {
const valid = await $axios.$post('/user/check_recover_code', { recover_code: code })
return { valid, code }
}
catch (e) {
return { valid: false }
}
},
methods: {
async change_password () {
try {
const res = await this.$axios.$post('/user/recover_password', { recover_code: this.code, password: this.new_password })
Message({
type: 'success',
message: this.$t('Password changed!')
})
} catch(e) {
Message({
type: 'warning',
message: e
})
}
}
}
}
</script>

63
pages/register.vue Normal file
View File

@@ -0,0 +1,63 @@
<template lang='pug'>
el-card
nuxt-link.float-right(to='/')
v-icon(name='times' color='red')
h5 {{$t('common.register')}}
el-form(@submit.native.prevent='register' method='POST' action='/api/user')
p(v-html="$t('register.description')")
el-input.mb-2(ref='email' v-model='user.email' type='email' required
:placeholder='$t("common.email")' autocomplete='email' name='email')
span(slot='prepend') @
el-input.mb-2(v-model='user.password' type="password" placeholder="Password" name='password' required)
v-icon(name='lock' slot='prepend')
el-input.mb-2(v-model='user.description' type="textarea" rows='3' :placeholder="$t('common.description')")
v-icon(name='envelope-open-text')
el-button(plain type="success" native-type='submit'
:disabled='disabled') {{$t('common.send')}} <v-icon name='chevron-right'/>
</template>
<script>
import { mapActions } from 'vuex'
import { Message } from 'element-ui'
import get from 'lodash/get'
export default {
name: 'Register',
data () {
return {
error: {},
user: { }
}
},
computed: {
disabled () {
if (process.server) return false
return !this.user.password || !this.user.email || !this.user.description
}
},
methods: {
...mapActions(['login']),
async register () {
try {
const { user } = await this.$axios.$post('/user', this.user)
Message({
message: this.$t(`register.${user.is_admin ? 'admin_' : ''}complete`),
type: 'success'
})
this.$router.replace("/")
} catch (e) {
const error = get(e, 'e.response.data.errors[0].message', String(e))
Message({
message: this.$t('register.error') + this.$t(error),
type: 'error'
})
}
}
}
}
</script>

46
pages/settings.vue Normal file
View File

@@ -0,0 +1,46 @@
<template lang="pug">
el-card
nuxt-link.float-right(to='/')
v-icon(name='times' color='red')
h5 {{$t('common.settings')}}
//- el-form
//- el-form-item {{$t('settings.change_password')}}
el-divider {{$t('settings.change_password')}}
el-input(v-model='password' type='password')
el-button(slot='append' @click='change' type='success') {{$t('common.send')}}
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
data () {
return {
password: '',
}
},
// computed: mapState(['user']),
// async asyncData ({ $axios, params }) {
// const user = await $axios.$get('/auth/user')
// user.mastodon_auth = ''
// return { user }
// },
methods: {
async change () {
if (!this.password) return
// this.$auth.user.password = this.password
const user_data = { id : this.$auth.user.id, password: this.password }
try {
const user = await this.$axios.$put('/user', user_data)
console.error(user)
} catch (e) {
console.log(e)
}
},
close (done) {
this.$router.back()
done()
}
}
}
</script>

7
plugins/README.md Normal file
View 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).

50
plugins/element-ui.js Normal file
View File

@@ -0,0 +1,50 @@
import Vue from 'vue'
import { Button, Select, Tag, Option, Table, FormItem, Card, Row, Col, Upload, Checkbox,
Form, Tabs, TabPane, Switch, Input, Loading, TimeSelect, Badge, ButtonGroup, Divider, Step, Steps,
TableColumn, ColorPicker, Pagination, Popover, Tooltip, Dialog, Calendar, Carousel, CarouselItem,
Container, Footer , Timeline, TimelineItem, Menu, MenuItem } from 'element-ui'
import localeEn from 'element-ui/lib/locale/lang/en'
import localeIt from 'element-ui/lib/locale/lang/it'
import locale from 'element-ui/lib/locale'
locale.use(localeIt)
export default () => {
Vue.use(Button)
Vue.use(Divider)
Vue.use(Step)
Vue.use(Steps)
Vue.use(Checkbox)
Vue.use(Upload)
Vue.use(ButtonGroup)
Vue.use(Calendar)
Vue.use(Row)
Vue.use(Col)
Vue.use(Carousel)
Vue.use(CarouselItem)
Vue.use(Badge)
Vue.use(Dialog)
Vue.use(Menu)
Vue.use(MenuItem)
Vue.use(Container)
Vue.use(Timeline)
Vue.use(TimelineItem)
Vue.use(Footer)
Vue.use(Tooltip)
Vue.use(Popover)
Vue.use(Card)
Vue.use(Select)
Vue.use(Tag)
Vue.use(Input)
Vue.use(Tabs)
Vue.use(TabPane)
Vue.use(Option)
Vue.use(Switch)
Vue.use(ColorPicker)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Pagination)
Vue.use(FormItem)
Vue.use(Form)
Vue.use(TimeSelect)
Vue.use(Loading.directive)
}

31
plugins/filters.js Normal file
View File

@@ -0,0 +1,31 @@
import Vue from 'vue'
import moment from 'dayjs'
import 'dayjs/locale/it'
moment.locale('it')
function short_hour(datetime) {
if (datetime.minute() === 0) {
return 'h' + datetime.format('HH')
} else {
return 'h' + datetime.format('HH:mm')
}
}
export default (a) => {
Vue.filter('linkify', value => value.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1">$1</a>'))
Vue.filter('datetime', value => moment(value).format('ddd, D MMMM HH:mm'))
Vue.filter('short_datetime', value => moment(value).format('D/MM HH:mm'))
Vue.filter('hour', value => moment(value).format('HH:mm'))
Vue.filter('day', value => moment(value).format('dddd, D MMMM'))
Vue.filter('month', value => moment(value).format('MMM'))
Vue.filter('event_when', event => {
const start = moment(event.start_datetime)
const end = moment(event.end_datetime)
if (event.multidate) {
return `${start.format('ddd, D MMMM')} (${short_hour(start)}) - ${end.format('ddd, D MMMM')} (${short_hour(end)})`
} else if (event.end_datetime && event.end_datetime !== event.start_datetime)
return `${start.format('ddd, D MMMM')} (${short_hour(start)}-${short_hour(end)}`
else
return `${start.format('dddd, D MMMM')} (${short_hour(start)})`
})
}

25
plugins/i18n.js Normal file
View File

@@ -0,0 +1,25 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import it from '@/locales/it.js'
Vue.use(VueI18n)
export default ({ app, store }) => {
// Set i18n instance on app
// This way we can use it in middleware and pages asyncData/fetch
app.i18n = new VueI18n({
locale: store.state.locale,
fallbackLocale: 'it',
messages: {
it
}
})
// app.i18n.path = (link) => {
// if (app.i18n.locale === app.i18n.fallbackLocale) {
// return `/${link}`
// }
// return `/${app.i18n.locale}/${link}`
// }
}

9
plugins/initialize.js Normal file
View File

@@ -0,0 +1,9 @@
// TOFIX: not needed in any case (eg. embed)
export default async ({ store, $axios }) => {
// const now = new Date()
// const events = await $axios.$get(`/event/${now.getMonth()}/${now.getFullYear()}`)
// store.commit('setEvents', events)
// const { tags, places } = await $axios.$get('/event/meta')
// store.commit('update', { tags, places })
}

8
plugins/v-calendar.js Normal file
View File

@@ -0,0 +1,8 @@
import Vue from 'vue'
import VCalendar from 'v-calendar'
export default () => {
Vue.use(VCalendar, {
firstDayOfWeek: 2
})
}

33
plugins/vue-awesome.js Normal file
View File

@@ -0,0 +1,33 @@
import Vue from 'vue'
import 'vue-awesome/icons/lock'
import 'vue-awesome/icons/user'
import 'vue-awesome/icons/plus'
import 'vue-awesome/icons/cog'
import 'vue-awesome/icons/tools'
import 'vue-awesome/icons/file-export'
import 'vue-awesome/icons/sign-out-alt'
import 'vue-awesome/icons/clock'
import 'vue-awesome/icons/map-marker-alt'
import 'vue-awesome/icons/file-alt'
import 'vue-awesome/icons/image'
import 'vue-awesome/icons/tag'
import 'vue-awesome/icons/users'
import 'vue-awesome/icons/calendar'
import 'vue-awesome/icons/edit'
import 'vue-awesome/icons/envelope-open-text'
import 'vue-awesome/icons/user-secret'
import 'vue-awesome/icons/question-circle'
import 'vue-awesome/icons/share'
import 'vue-awesome/icons/comment'
import 'vue-awesome/icons/comments'
import 'vue-awesome/icons/tags'
import 'vue-awesome/icons/chevron-right'
import 'vue-awesome/icons/chevron-left'
import 'vue-awesome/icons/search'
import 'vue-awesome/icons/times'
import Icon from 'vue-awesome/components/Icon'
export default () => {
Vue.component('v-icon', Icon)
}

9
plugins/vuex-persist.js Normal file
View File

@@ -0,0 +1,9 @@
// // ~/plugins/vuex-persist.js
// import VuexPersistence from 'vuex-persist'
// export default ({ store }) => {
// const per = new VuexPersistence({
// reducer: state => ({ logged: state.logged, user: state.user, token: state.token })
// }).plugin(store)
// store.dispatch('login')
// }

52
server/api/auth.js Normal file
View File

@@ -0,0 +1,52 @@
const { Op } = require('sequelize')
const { user: User } = require('./models')
const Settings = require('./controller/settings')
const Auth = {
async fillUser(req, res, next) {
if (!req.user) return next()
req.user = await User.findOne({
where: { id: { [Op.eq]: req.user.id }, is_active: true }
}).catch(e => {
res.sendStatus(404)
return next(false)
})
next()
},
async isAuth(req, res, next) {
if (!req.user) {
return res
.status(403)
.send({ message: 'Failed to authenticate token ' })
}
req.user = await User.findOne({
where: { id: { [Op.eq]: req.user.id }, is_active: true }
})
if (!req.user) {
return res
.status(403)
.send({ message: 'Failed to authenticate token ' + err })
}
next()
},
isAdmin(req, res, next) {
if (!req.user) {
return res
.status(403)
.send({ message: 'Failed to authenticate token ' })
}
if (req.user.is_admin && req.user.is_active) return next()
return res.status(403).send({ message: 'Admin needed' })
},
async adminOrFirstRun(req, res, next) {
if (req.user && req.user.is_admin && req.user.is_active) return next()
const settings = await Settings.settings()
if (!settings.firstRun) {
return next()
}
}
}
module.exports = Auth

View File

@@ -0,0 +1,95 @@
const fs = require('fs')
const path = require('path')
const moment = require('moment')
const { event: Event, comment: Comment, tag: Tag } = require('../model')
const config = require('../../config').SHARED_CONF
const Mastodon = require('mastodon-api')
const settingsController = require('./settings')
moment.locale('it')
const botController = {
bot: null,
async initialize() {
console.error('dentro bot inizialiteds')
const settings = await settingsController.settings()
if (!settings.mastodon_auth || !settings.mastodon_auth.access_token) return
const mastodon_auth = settings.mastodon_auth
botController.bot = new Mastodon({
access_token: mastodon_auth.access_token,
api_url: `https://${mastodon_auth.instance}/api/v1`
})
const listener = botController.bot.stream('/streaming/direct')
listener.on('message', botController.message)
listener.on('error', botController.error)
// const botUsers = await User.findAll({ where: { mastodon_auth: { [Op.ne]: null } } })
// console.log(botUsers)
// botController.bots = botUsers.map(user => {
// console.log('initialize bot ', user.name)
// console.log('.. ', user.mastodon_auth)
// const { client_id, client_secret, access_token } = user.mastodon_auth
// const bot = new Mastodon({ access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
// const listener = bot.stream('streaming/direct')
// listener.on('message', botController.message)
// return { email: user.email, bot }
// })
// console.log(botController.bots)
// },
// add (user, token) {
// const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
// const listener = bot.stream('streaming/direct')
// listener.on('message', botController.message)
// listener.on('error', botController.error)
// botController.bots.push({ email: user.email, bot })
},
async post(mastodon_auth, event) {
const { access_token, instance } = mastodon_auth
const bot = new Mastodon({ access_token, api_url: `https://${instance}/api/v1/` })
const status = `${event.title} @ ${event.place.name} ${moment(event.start_datetime).format('ddd, D MMMM HH:mm')} -
${event.description.length > 200 ? event.description.substr(0, 200) + '...' : event.description} - ${event.tags.map(t => '#' + t.tag).join(' ')} ${config.baseurl}/event/${event.id}`
let media
if (event.image_path) {
const file = path.join(__dirname, '..', '..', '..', 'uploads', event.image_path)
if (fs.statSync(file)) {
media = await bot.post('media', { file: fs.createReadStream(file) })
}
}
return bot.post('statuses', { status, visibility: 'direct', media_ids: media ? [media.data.id] : [] })
},
// TOFIX: enable message deletion
async message(msg) {
const replyid = msg.data.in_reply_to_id || msg.data.last_status.in_reply_to_id
if (!replyid) return
const event = await Event.findOne({ where: { activitypub_id: replyid } })
if (!event) {
// check for comment..
// const comment = await Comment.findOne( {where: { }})
return
}
const comment = await Comment.create({
activitypub_id: msg.data.last_status.id,
// text: msg.data.last_status.content,
data: msg.data
// author: msg.data.accounts[0].username
})
event.addComment(comment)
// const comment = await Comment.findOne( { where: {activitypub_id: msg.data.in_reply_to}} )
// console.log('dentro message ', data)
// add comment to specified event
// let comment
// if (!event) {
// const comment = await Comment.findOne({where: {activitypub_id: req.body.id}, include: Event})
// event = comment.event
// }
// const comment = new Comment(req.body)
// event.addComment(comment)
},
error(err) {
console.log('error ', err)
}
}
// setTimeout(botController.initialize, 2000)
module.exports = botController

View File

@@ -0,0 +1,194 @@
const crypto = require('crypto')
const moment = require('moment')
const { Op } = require('sequelize')
const lodash = require('lodash')
const { event: Event, comment: Comment, tag: Tag, place: Place, notification: Notification } = require('../models')
const Sequelize = require('sequelize')
const eventController = {
async addComment(req, res) {
// comment could be added to an event or to another comment
let event = await Event.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } } })
if (!event) {
const comment = await Comment.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } }, include: Event })
event = comment.event
}
const comment = new Comment(req.body)
event.addComment(comment)
res.json(comment)
},
async getMeta(req, res) {
const places = await Place.findAll({
order: [[Sequelize.literal('weigth'), 'DESC']],
attributes: {
include: [[Sequelize.fn('count', Sequelize.col('events.placeId')) , 'weigth']], // <---- Here you will get the total count of user
exclude: ['weigth', 'createdAt', 'updatedAt']
},
include: [{ model: Event, attributes: [] }],
group: ['place.id']
})
const tags = await Tag.findAll({
order: [['weigth', 'DESC']],
includeIgnoreAttributes: false,
attributes: {
exclude: ['createdAt', 'updatedAt']
}
})
res.json({ tags, places })
},
async getNotifications(event) {
function match(event, filters) {
// matches if no filter specified
if (!filters) return true
// check for visibility
if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) return false
if (!filters.tags && !filters.places) return true
if (!filters.tags.length && !filters.places.length) return true
if (filters.tags.length) {
const m = lodash.intersection(event.tags.map(t => t.tag), filters.tags)
if (m.length > 0) return true
}
if (filters.places.length) {
if (filters.places.find(p => p === event.place.name)) {
return true
}
}
}
const notifications = await Notification.findAll()
// get notification that matches with selected event
return notifications.filter(notification => match(event, notification.filters))
},
async updateTag(req, res) {
const tag = await Tag.findByPk(req.body.tag)
if (tag) {
res.json(await tag.update(req.body))
} else {
res.sendStatus(404)
}
},
async updatePlace(req, res) {
const place = await Place.findByPk(req.body.id)
await place.update(req.body)
res.json(place)
},
async get(req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id, { include:
[
Tag,
Comment,
{ model: Place, attributes: ['name', 'address'] }
],
order: [ [Comment, 'id', 'DESC'], [Tag, 'weigth', 'DESC'] ]
})
res.json(event)
},
async confirm(req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id)
try {
await event.update({ is_visible: true })
// insert notification
const notifications = await eventController.getNotifications(event)
await event.setNotifications(notifications)
res.sendStatus(200)
} catch (e) {
res.sendStatus(404)
}
},
async unconfirm(req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id)
try {
await event.update({ is_visible: false })
res.sendStatus(200)
} catch (e) {
res.sendStatus(404)
}
},
async getUnconfirmed(req, res) {
const events = await Event.findAll({
where: {
is_visible: false
},
order: [['start_datetime', 'ASC']],
include: [Tag, Place]
})
res.json(events)
},
async addNotification(req, res) {
try {
const notification = {
filters: { is_visible: true },
email: req.body.email,
type: 'mail',
remove_code: crypto.randomBytes(16).toString('hex')
}
await Notification.create(notification)
res.sendStatus(200)
} catch (e) {
res.sendStatus(404)
}
},
async delNotification(req, res) {
const remove_code = req.params.code
try {
const notification = await Notification.findOne({ where: { remove_code: { [Op.eq]: remove_code } } })
await notification.destroy()
} catch (e) {
return res.sendStatus(404)
}
res.sendStatus(200)
},
async getAll(req, res) {
// this is due how v-calendar shows dates
const start = moment().year(req.params.year).month(req.params.month)
.startOf('month').startOf('isoWeek')
let end = moment().year(req.params.year).month(req.params.month).endOf('month')
const shownDays = end.diff(start, 'days')
if (shownDays <= 34) end = end.add(1, 'week')
end = end.endOf('isoWeek')
const events = await Event.findAll({
where: {
is_visible: true,
[Op.and]: [
{ start_datetime: { [Op.gte]: start } },
{ start_datetime: { [Op.lte]: end } }
]
},
order: [
['start_datetime', 'ASC'],
[Tag, 'weigth', 'DESC']
],
include: [
// { model: User, required: false },
// { type: Comment, required: false, attributes: ['']
{ model: Tag, required: false, attributes: ['tag', 'weigth', 'color'] },
{ model: Place, required: false, attributes: ['id', 'name', 'address'] }
]
})
res.json(events)
}
}
module.exports = eventController

View File

@@ -0,0 +1,70 @@
const { event: Event, place: Place } = require('../models')
const { Op } = require('sequelize')
const moment = require('moment')
const ics = require('ics')
const exportController = {
async export(req, res) {
console.log('type ', req.params.type)
console.error(req)
const type = req.params.type
const tags = req.query.tags
const places = req.query.places
const whereTag = {}
const wherePlace = {}
const yesterday = moment().subtract('1', 'day')
if (tags) {
whereTag.tag = tags.split(',')
}
if (places) {
wherePlace.id = places.split(',')
}
const events = await Event.findAll({
order: ['start_datetime'],
where: {
is_visible: true,
start_datetime: { [Op.gte]: yesterday },
placeId: places.split(',')
},
attributes: {
exclude: ['createdAt', 'updatedAt']
},
include: [{ model: Place, attributes: ['name', 'id', 'address', 'weigth'] }]
})
switch (type) {
case 'feed':
return exportController.feed(res, events.slice(0, 20))
case 'ics':
return exportController.ics(res, events)
case 'json':
return res.json(events)
}
},
feed(res, events) {
res.type('application/rss+xml; charset=UTF-8')
res.render('feed/rss.pug', { events, config: process.env.config, moment })
},
ics(res, events) {
const eventsMap = events.map(e => {
const tmpStart = moment(e.start_datetime)
const tmpEnd = moment(e.end_datetime)
const start = [tmpStart.year(), tmpStart.month() + 1, tmpStart.date(), tmpStart.hour(), tmpStart.minute()]
const end = [tmpEnd.year(), tmpEnd.month() + 1, tmpEnd.date(), tmpEnd.hour(), tmpEnd.minute()]
return {
start,
end,
title: e.title,
description: e.description,
location: e.place.name + ' ' + e.place.address
}
})
res.type('text/calendar; charset=UTF-8')
const { error, value } = ics.createEvents(eventsMap)
res.send(value)
}
}
module.exports = exportController

View File

@@ -0,0 +1,61 @@
const Mastodon = require('mastodon-api')
const { setting: Setting } = require('../models')
const baseurl = process.env.baseurl
const settingsController = {
async setAdminSetting(key, value) {
await Setting.findOrCreate({ where: { key },
defaults: { value } })
.spread((settings, created) => {
if (!created) return settings.update({ value })
})
},
async getAdminSettings(req, res) {
const settings = await settingsController.settings()
res.json(settings)
},
async getAuthURL(req, res) {
const instance = req.body.instance
const callback = `${baseurl}/api/settings/oauth`
const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`,
'gancio', 'read write', callback)
const url = await Mastodon.getAuthorizationUrl(client_id, client_secret,
`https://${instance}`, 'read write', callback)
await settingsController.setAdminSetting('mastodon_auth', { client_id, client_secret, instance })
res.json(url)
},
async code(req, res) {
const code = req.query.code
let client_id, client_secret, instance
const callback = `${baseurl}/api/settings/oauth`
const settings = await settingsController.settings()
({ client_id, client_secret, instance } = settings.mastodon_auth)
try {
const token = await Mastodon.getAccessToken(client_id, client_secret, code,
`https://${instance}`, callback)
const mastodon_auth = { client_id, client_secret, access_token: token, instance }
await settingsController.setAdminSetting('mastodon_auth', mastodon_auth)
res.redirect('/admin')
} catch (e) {
res.json(e)
}
},
async settings() {
const settings = await Setting.findAll()
return settings
}
}
module.exports = settingsController

View File

@@ -0,0 +1,250 @@
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const jwt = require('jsonwebtoken')
const { Op } = require('sequelize')
const jsonwebtoken = require('jsonwebtoken')
const mail = require('../mail')
const { user: User, event: Event, tag: Tag, place: Place } = require('../models')
const eventController = require('./event')
const config = require('../../config')
const userController = {
async login(req, res) {
// find the user
const user = await User.findOne({ where: { email: { [Op.eq]: req.body && req.body.email } } })
if (!user) {
res.status(403).json({ success: false, message: 'auth.fail' })
} else if (user) {
if (!user.is_active) {
res.status(403).json({ success: false, message: 'auth.not_confirmed' })
// check if password matches
} else if (!await user.comparePassword(req.body.password)) {
res.status(403).json({ success: false, message: 'auth.fail' })
} else {
// if user is found and password is right
// create a token
const accessToken = jsonwebtoken.sign(
{
id: user.id,
email: user.email,
scope: [user.is_admin ? 'admin' : 'user']
},
config.SECRET_CONF.secret
)
res.json({ token: accessToken })
}
}
},
async logout(req, res) {
},
async setToken(req, res) {
req.user.mastodon_auth = req.body
await req.user.save()
res.json(req.user)
},
async delEvent(req, res) {
const event = await Event.findByPk(req.params.id)
// check if event is mine (or user is admin)
if (event && (req.user.is_admin || req.user.id === event.userId)) {
if (event.image_path) {
const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path)
const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path)
try {
await fs.unlink(old_path)
await fs.unlink(old_thumb_path)
} catch (e) {
console.error(e)
}
}
await event.destroy()
res.sendStatus(200)
} else {
res.sendStatus(403)
}
},
// ADD EVENT
async addEvent(req, res) {
const body = req.body
const eventDetails = {
title: body.title,
description: body.description.replace(/(<([^>]+)>)/ig, ''),
multidate: body.multidate,
start_datetime: body.start_datetime,
end_datetime: body.end_datetime,
// publish this event if authenticated
is_visible: !!req.user
}
if (req.file) {
eventDetails.image_path = req.file.filename
}
let event = await Event.create(eventDetails)
// create place if needs to
let place
try {
place = await Place.findOrCreate({ where: { name: body.place_name },
defaults: { address: body.place_address } })
.spread((place, created) => place)
await event.setPlace(place)
} catch (e) {
console.error(e)
}
// create/assign tags
if (body.tags) {
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
await event.addTags(tags)
}
if (req.user) await req.user.addEvent(event)
event = await Event.findByPk(event.id, { include: [User, Tag, Place] })
// insert notifications
const notifications = await eventController.getNotifications(event)
await event.setNotifications(notifications)
return res.json(event)
},
async updateEvent(req, res) {
const body = req.body
const event = await Event.findByPk(body.id)
if (!req.user.is_admin && event.userId !== req.user.id) {
return res.sendStatus(403)
}
if (req.file) {
if (event.image_path) {
const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path)
const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path)
await fs.unlink(old_path, e => console.error(e))
await fs.unlink(old_thumb_path, e => console.error(e))
}
body.image_path = req.file.filename
}
body.description = body.description
.replace(/(<([^>]+)>)/ig, '') // remove all tags from description
// .replace(/(https?:\/\/[^\s]+)/g, '<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 forgotPassword(req, res) {
const email = req.body.email
const user = await User.findOne({ where: { email: { [Op.eq]: email } } })
if (!user) return res.sendStatus(200)
user.recover_code = crypto.randomBytes(16).toString('hex')
mail.send(user.email, 'recover', { user, config: config.SHARED_CONF })
await user.save()
res.sendStatus(200)
},
async checkRecoverCode(req, res) {
const recover_code = req.body.recover_code
if (!recover_code) return res.sendStatus(400)
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
if (!user) return res.sendStatus(400)
res.json(user)
},
async updatePasswordWithRecoverCode(req, res) {
const recover_code = req.body.recover_code
const password = req.body.password
if (!recover_code || !password) return res.sendStatus(400)
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
if (!user) return res.sendStatus(400)
user.password = password
try {
await user.save()
res.sendStatus(200)
} catch (e) {
res.sendStatus(400)
}
},
current(req, res) {
if (req.user) { res.json(req.user) } else { res.sendStatus(404) }
},
async getAll(req, res) {
const users = await User.findAll({
order: [['createdAt', 'DESC']]
})
res.json(users)
},
async update(req, res) {
const user = await User.findByPk(req.body.id)
console.error(req.body.id)
if (user) {
if (!user.is_active && req.body.is_active) {
await mail.send(user.email, 'confirm', { user, config: config.SHARED_CONF })
}
await user.update(req.body)
res.json(user)
} else {
res.sendStatus(400)
}
},
async register(req, res) {
const n_users = await User.count()
try {
// the first registered user will be an active admin
if (n_users === 0) {
req.body.is_active = req.body.is_admin = true
} else {
req.body.is_active = false
}
const user = await User.create(req.body)
try {
mail.send([user.email, config.SECRET_CONF.admin], 'register', { user, config: config.SHARED_CONF })
} catch (e) {
return res.status(400).json(e)
}
const payload = {
id: user.id,
email: user.email,
scope: [user.is_admin ? 'admin' : 'user']
}
const token = jwt.sign(payload, config.SECRET_CONF.secret)
res.json({ token, user })
} catch (e) {
res.status(404).json(e)
}
}
}
module.exports = userController

107
server/api/index.js Normal file
View File

@@ -0,0 +1,107 @@
const express = require('express')
const multer = require('multer')
const cookieParser = require('cookie-parser')
const bodyParser = require('body-parser')
const expressJwt = require('express-jwt')
const config = require('../config')
const { fillUser, isAuth, isAdmin } = require('./auth')
const eventController = require('./controller/event')
const exportController = require('./controller/export')
const userController = require('./controller/user')
const settingsController = require('./controller/settings')
const storage = require('./storage')({
destination: 'uploads/'
})
const upload = multer({ storage })
const api = express.Router()
api.use(cookieParser())
api.use(bodyParser.urlencoded({ extended: false }))
api.use(bodyParser.json())
const jwt = expressJwt({
secret: config.SECRET_CONF.secret,
credentialsRequired: false
})
function errorHandler(fn) {
return async (req, res) => {
try {
await fn(req, res)
} catch (e) {
console.error(String(e))
return res.status(500).json(e)
}
}
}
// AUTH
api.post('/auth/login', userController.login)
api.post('/auth/logout', userController.logout)
api.get('/auth/user', jwt, fillUser, userController.current)
api.post('/user/recover', userController.forgotPassword)
api.post('/user/check_recover_code', userController.checkRecoverCode)
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
api
.route('/user')
// register
.post(userController.register)
// get current user
// .get(isAuth, userController.current)
// update user (eg. confirm)
.put(jwt, isAuth, isAdmin, userController.update)
// get all users
api.get('/users', jwt, isAuth, isAdmin, userController.getAll)
// update a tag (modify color)
api.put('/tag', jwt, isAuth, isAdmin, eventController.updateTag)
// update a place (modify address..)
api.put('/place', jwt, isAuth, isAdmin, eventController.updatePlace)
api
.route('/user/event')
// add event
.post(jwt, fillUser, upload.single('image'), userController.addEvent)
// update event
.put(jwt, isAuth, upload.single('image'), userController.updateEvent)
// remove event
api.delete('/user/event/:id', jwt, isAuth, userController.delEvent)
// get tags/places
api.get('/event/meta', eventController.getMeta)
// get unconfirmed events
api.get('/event/unconfirmed', jwt, isAuth, isAdmin, eventController.getUnconfirmed)
// add event notification
api.post('/event/notification', eventController.addNotification)
api.delete('/event/notification/:code', eventController.delNotification)
api.get('/settings', jwt, fillUser, isAdmin, settingsController.getAdminSettings)
api.post('/settings', jwt, fillUser, isAdmin, settingsController.setAdminSetting)
// get event
api.get('/event/:event_id', eventController.get)
// confirm event
api.get('/event/confirm/:event_id', jwt, isAuth, isAdmin, eventController.confirm)
api.get('/event/unconfirm/:event_id', jwt, isAuth, isAdmin, eventController.unconfirm)
// export events (rss/ics)
api.get('/export/:type', exportController.export)
// get events in this range
api.get('/event/:month/:year', errorHandler(eventController.getAll))
// mastodon oauth auth
api.post('/settings/getauthurl', jwt, isAuth, isAdmin, settingsController.getAuthURL)
api.get('/settings/oauth', jwt, isAuth, isAdmin, settingsController.code)
module.exports = api

49
server/api/mail.js Normal file
View File

@@ -0,0 +1,49 @@
const Email = require('email-templates')
const path = require('path')
const moment = require('moment')
const config = require('../config')
moment.locale(config.SHARED_CONF.locale)
const mail = {
send(addresses, template, locals) {
const email = new Email({
views: { root: path.join(__dirname, '..', 'emails') },
htmlToText: false,
juice: true,
juiceResources: {
preserveImportant: true,
webResources: {
relativeTo: path.join(__dirname, '..', 'emails')
}
},
message: {
from: `${config.SHARED_CONF.title} <${config.SECRET_CONF.smtp.auth.user}>`
},
send: true,
i18n: {
directory: path.join(__dirname, '..', '..', 'locales', 'email'),
defaultLocale: config.SHARED_CONF.locale
},
transport: config.SECRET_CONF.smtp
})
const msg = {
template,
message: {
to: addresses,
bcc: config.SECRET_CONF.admin
},
locals: {
...locals,
locale: config.SHARED_CONF.locale,
config: config.SHARED_CONF,
datetime: datetime => moment(datetime).format('ddd, D MMMM HH:mm')
}
}
return email.send(msg)
.catch(e => {
console.error(e)
})
}
}
module.exports = mail

View File

@@ -0,0 +1,13 @@
'use strict'
module.exports = (sequelize, DataTypes) => {
const comment = sequelize.define('comment', {
activitypub_id: DataTypes.BIGINT,
data: DataTypes.JSON
}, {})
comment.associate = function (models) {
comment.belongsTo(models.event)
// Event.hasMany(Comment)
// associations can be defined here
}
return comment
};

View File

@@ -0,0 +1,31 @@
'use strict'
module.exports = (sequelize, DataTypes) => {
const event = sequelize.define('event', {
title: DataTypes.STRING,
slug: DataTypes.STRING,
description: DataTypes.TEXT,
multidate: DataTypes.BOOLEAN,
start_datetime: {
type: DataTypes.DATE,
index: true
},
end_datetime: DataTypes.DATE,
image_path: DataTypes.STRING,
is_visible: DataTypes.BOOLEAN,
activitypub_id: {
type: DataTypes.BIGINT,
index: true
}
}, {})
event.associate = function (models) {
event.belongsTo(models.place)
event.belongsTo(models.user)
event.belongsToMany(models.tag, { through: 'event_tags' })
event.belongsToMany(models.notification, { through: 'event_notification' })
event.hasMany(models.comment)
// Tag.belongsToMany(Event, { through: 'tagEvent' })
// Event.hasMany(models.Tag)
// associations can be defined here
}
return event
};

View File

@@ -0,0 +1,16 @@
'use strict'
module.exports = (sequelize, DataTypes) => {
const eventNotification = sequelize.define('eventNotification', {
status: {
type: DataTypes.ENUM,
values: ['new', 'sent', 'error'],
defaultValue: 'new',
index: true
}
}, {})
eventNotification.associate = function (models) {
// associations can be defined here
}
return eventNotification
}

View File

@@ -0,0 +1,31 @@
const argv = require('yargs').argv
const fs = require('fs')
const path = require('path')
const Sequelize = require('sequelize')
const config_path = path.resolve(argv.config || './config.js')
const basename = path.basename(__filename)
const config = require(config_path).SECRET_CONF.db
const db = {}
const sequelize = new Sequelize(config)
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')
})
.forEach(file => {
const model = sequelize.import(path.join(__dirname, file))
db[model.name] = model
})
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db)
}
})
db.sequelize = sequelize
db.Sequelize = Sequelize
module.exports = db

View File

@@ -0,0 +1,17 @@
'use strict'
module.exports = (sequelize, DataTypes) => {
const notification = sequelize.define('notification', {
filters: DataTypes.JSON,
email: DataTypes.STRING,
remove_code: DataTypes.STRING,
type: {
type: DataTypes.ENUM,
values: ['mail', 'admin_email', 'mastodon']
}
}, {})
notification.associate = function (models) {
notification.belongsToMany(models.event, { through: 'event_notification' })
// associations can be defined here
}
return notification
}

View File

@@ -0,0 +1,15 @@
'use strict'
module.exports = (sequelize, DataTypes) => {
const place = sequelize.define('place', {
name: DataTypes.STRING,
address: DataTypes.STRING,
weigth: DataTypes.INTEGER
}, {})
place.associate = function (models) {
// associations can be defined here
place.hasMany(models.event)
}
return place
}

View File

@@ -0,0 +1,14 @@
'use strict'
module.exports = (sequelize, DataTypes) => {
const setting = sequelize.define('setting', {
key: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
index: true
},
value: DataTypes.JSON
}, {})
return setting
}

19
server/api/models/tag.js Normal file
View File

@@ -0,0 +1,19 @@
'use strict'
module.exports = (sequelize, DataTypes) => {
const tag = sequelize.define('tag', {
tag: {
type: DataTypes.STRING,
index: true,
primaryKey: true
},
weigth: DataTypes.INTEGER,
color: DataTypes.STRING
}, {})
tag.associate = function (models) {
tag.belongsToMany(models.event, { through: 'event_tags' })
// associations can be defined here
}
return tag
};

43
server/api/models/user.js Normal file
View File

@@ -0,0 +1,43 @@
'use strict'
const bcrypt = require('bcrypt')
module.exports = (sequelize, DataTypes) => {
const user = sequelize.define('user', {
email: {
type: DataTypes.STRING,
unique: true,
index: true,
allowNull: false
},
description: DataTypes.TEXT,
password: DataTypes.STRING,
recover_code: DataTypes.STRING,
is_admin: DataTypes.BOOLEAN,
is_active: DataTypes.BOOLEAN
}, {
defaultScope: {
attributes: { exclude: ['password', 'recover_code'] }
}
})
user.associate = function (models) {
// associations can be defined here
user.hasMany(models.event)
}
user.prototype.comparePassword = async function (pwd) {
if (!this.password) return false
const ret = await bcrypt.compare(pwd, this.password)
return ret
}
user.beforeSave(async (user, options) => {
if (user.changed('password')) {
const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(user.password, salt)
user.password = hash
}
})
return user
};

62
server/api/storage.js Normal file
View 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: 90 })
const thumbnailer = sharp().resize(400).jpeg({ quality: 90 })
file.stream.pipe(thumbnailer).pipe(thumbStream)
thumbStream.on('error', e => console.log('thumbStream error ', e))
file.stream.pipe(resizer).pipe(outStream)
outStream.on('error', cb)
outStream.on('finish', function () {
cb(null, {
destination,
filename,
path: finalPath,
size: outStream.bytesWritten
})
})
})
}
DiskStorage.prototype._removeFile = function _removeFile(req, file, cb) {
let path = file.path
delete file.destination
delete file.filename
delete file.path
fs.unlink(path, cb)
}
module.exports = function (opts) {
return new DiskStorage(opts)
}

View File

@@ -0,0 +1,23 @@
doctype xml
rss(version='2.0')
channel
title #{config.title}
link #{config.baseurl}
description #{config.description}
language #{config.locale}
//- if events.length
lastBuildDate= new Date(posts[0].publishedAt).toUTCString()
each event in events
item
title= event.title
link #{config.baseurl}/event/#{event.id}
description
| <![CDATA[
| <h4>#{event.title}</h4>
| <strong>#{event.place.name} - #{event.place.address}</strong>
| #{moment(event.start_datetime).format("dddd, D MMMM HH:mm")}<br/>
| <img src="#{config.apiurl}/../uploads/#{event.image_path}"/>
| <pre>!{event.description}</pre>
| ]]>
pubDate= new Date(event.createdAt).toUTCString()
guid(isPermaLink='false') #{config.baseurl}/event/#{event.id}

View File

@@ -0,0 +1,4 @@
p= t('confirm_email')
hr
small #{config.baseurl}

View File

@@ -0,0 +1,18 @@
h3 #{event.title}
p Dove: #{event.place.name} - #{event.place.address}
p Quando: #{datetime(event.start_datetime)}
br
if event.image_path
<img style="width: 100%" src="#{config.apiurl}/uploads/#{event.image_path}" />
p #{event.description}
each tag in event.tags
span ##{tag.tag}
br
<a href="#{config.baseurl}/event/#{event.id}">#{config.baseurl}/event/#{event.id}</a>
hr
if to_confirm
p Puoi confermare questo evento <a href="#{config.baseurl}/admin/confirm/#{event.id}">qui</a>
else
p Puoi eliminare queste notifiche <a href="#{config.baseurl}/del_notification/#{notification.remove_code}">qui</a>
<a href="#{config.baseurl}">#{config.title} - #{config.description}</a>

View File

@@ -0,0 +1 @@
= `[${config.title}] ${event.title} @${event.place.name} ${datetime(event.start_datetime)}`

8
server/emails/mail.css Normal file
View File

@@ -0,0 +1,8 @@
table {
width: 100%;
border-collapse: collapse;
}
table, th, td {
border: 1px solid #555;
}

View File

@@ -0,0 +1,3 @@
p= t('mail.recover')
<a href="#{config.baseurl}/recover/#{user.recover_code}">#{t('press here')}</a>

View File

@@ -0,0 +1 @@
= `[Gancio] Richiesta password recovery`

View File

@@ -0,0 +1,6 @@
p= t('registration_email')
hr
small #{config.title} / #{config.description}
br
small #{config.baseurl}

View File

@@ -0,0 +1 @@
| [Gancio] #{t('register.request')}

View File

@@ -0,0 +1 @@
| dioicane ciao

41
server/firstrun.js Normal file
View File

@@ -0,0 +1,41 @@
// check config.js existance
const fs = require('fs')
const path = require('path')
const argv = require('yargs').argv
const config_path = path.resolve(argv.config || './config.js')
if (!fs.existsSync(config_path)) {
console.error(`Configuration file not found at '${config_path}. Please copy 'config.example.js' and modify it.`)
process.exit(1)
}
const { SECRET_CONF, SHARED_CONF } = require(config_path)
if (!SECRET_CONF.secret) {
console.error(`Please specify a random 'secret' in '${config_path}'!`)
process.exit(1)
}
const Sequelize = require('sequelize')
let db
try {
db = new Sequelize(SECRET_CONF.db)
} catch (e) {
console.error(`DB Error: check '${SHARED_CONF.env}' configuration.\n (sequelize error -> ${e})`)
process.exit(1)
}
// return db existence
module.exports = db.authenticate()
.then(() => {
require('./api/models')
if (SHARED_CONF.env === 'development') {
console.error('DB Force sync')
return db.sync({ force: true })
}
})
.catch(e => {
console.error(e)
console.error(`DB Error: check '${SHARED_CONF.env}' configuration\n (sequelize error -> ${e})`)
process.exit(1)
})

39
server/index.js Normal file
View File

@@ -0,0 +1,39 @@
#!/bin/env node
const path = require('path')
const express = require('express')
const consola = require('consola')
const morgan = require('morgan')
const { Nuxt, Builder } = require('nuxt')
const firstRun = require('./firstrun')
// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
const app = express()
async function start() {
// Init Nuxt.js
const nuxt = new Nuxt(config)
const { host, port } = nuxt.options.server
// Build only in dev mode
if (config.dev) {
const builder = new Builder(nuxt)
await builder.build()
} else {
await nuxt.ready()
}
// Give nuxt middleware to express
app.use(morgan('dev'))
app.use('/media/', express.static(path.join(__dirname, '..', 'uploads')))
app.use(nuxt.render)
// Listen the server
app.listen(port, host)
consola.ready({
message: `Server listening on http://${host}:${port}`,
badge: true
})
}
firstRun.then(start)

View File

@@ -0,0 +1,45 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
email: {
type: Sequelize.STRING,
unique: true,
index: true,
allowNull: false
},
description: {
type: Sequelize.TEXT
},
password: {
type: Sequelize.STRING
},
recover_code: {
type: Sequelize.STRING
},
is_admin: {
type: Sequelize.BOOLEAN
},
is_active: {
type: Sequelize.BOOLEAN
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('users');
}
};

View File

@@ -0,0 +1,33 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('places', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING
},
address: {
type: Sequelize.STRING
},
weigth: {
type: Sequelize.INTEGER
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('places');
}
};

View File

@@ -0,0 +1,66 @@
'use strict'
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('events', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
title: {
type: Sequelize.STRING
},
slug: {
type: Sequelize.STRING,
index: true
},
description: {
type: Sequelize.TEXT
},
multidate: {
type: Sequelize.BOOLEAN
},
start_datetime: {
type: Sequelize.DATE
},
end_datetime: {
type: Sequelize.DATE
},
image_path: {
type: Sequelize.STRING
},
is_visible: {
type: Sequelize.BOOLEAN
},
activitypub_id: {
type: Sequelize.BIGINT
},
userId: {
type: Sequelize.INTEGER,
references: {
model: 'users',
key: 'id'
}
},
placeId: {
type: Sequelize.INTEGER,
references: {
model: 'places',
key: 'id'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('events')
}
}

View File

@@ -0,0 +1,37 @@
'use strict'
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('notifications', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
filters: {
type: Sequelize.JSON
},
email: {
type: Sequelize.STRING
},
remove_code: {
type: Sequelize.STRING
},
type: {
type: Sequelize.ENUM,
values: ['mail', 'admin_email', 'mastodon']
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('notifications')
}
}

View File

@@ -0,0 +1,29 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('tags', {
tag: {
type: Sequelize.STRING,
allowNull: false,
primaryKey: true
},
weigth: {
type: Sequelize.INTEGER
},
color: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('tags');
}
};

View File

@@ -0,0 +1,37 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('event_notification', {
eventId: {
type: Sequelize.INTEGER,
references: {
model: 'events',
key: 'id'
}
},
notificationId: {
type: Sequelize.INTEGER,
references: {
model: 'notifications',
key: 'id'
}
},
status: {
type: Sequelize.ENUM,
values: ['new', 'sent', 'error'],
defaultValue: 'new',
index: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('event_notification')
}
}

View File

@@ -0,0 +1,38 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('comments', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
eventId: {
type: Sequelize.INTEGER,
references: {
model: 'events',
key: 'id'
}
},
activitypub_id: {
type: Sequelize.BIGINT,
index: true,
},
data: {
type: Sequelize.JSON
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('comments');
}
};

View File

@@ -0,0 +1,27 @@
'use strict'
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('settings', {
key: {
type: Sequelize.STRING,
primaryKey: true,
allowNull: false,
index: true
},
value: {
type: Sequelize.JSON
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('settings')
}
}

View File

@@ -0,0 +1,32 @@
'use strict'
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('event_tags', {
eventId: {
type: Sequelize.INTEGER,
references: {
model: 'events',
key: 'id'
}
},
tagTag: {
type: Sequelize.STRING,
references: {
model: 'tags',
key: 'tag'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('event_tags')
}
}

66
server/notifier.js Normal file
View File

@@ -0,0 +1,66 @@
// const mail = require('./mail')
const bot = require('./api/controller/bot')
const settingsController = require('./api/controller/settings')
const config = require('./config.js')
const { Event, Notification, EventNotification,
User, Place, Tag } = require('./api/models')
let settings
async function sendNotification(notification, event, eventNotification) {
const promises = []
switch (notification.type) {
// case 'mail':
// return mail.send(notification.email, 'event', { event, config, notification })
// case 'admin_email':
// const admins = await User.findAll({ where: { is_admin: true } })
// const admin_emails = admins.map(admin => admin.email)
// return mail.send(admin_emails, 'event', { event, to_confirm: true, notification })
case 'mastodon':
// instance publish
if (settings.mastodon_auth.instance && settings.mastodon_auth.access_token) {
const b = bot.post(settings.mastodon_auth, event).then(b => {
event.activitypub_id = b.data.id
// event.activitypub_ids.push(b.data.id)
return event.save()
})
promises.push(b)
}
}
return Promise.all(promises)
}
async function notify() {
console.error('dentro il loop di notify')
settings = await settingsController.settings()
// get all event notification in queue
const eventNotifications = await EventNotification.findAll({ where: { status: 'new' } })
const promises = eventNotifications.map(async e => {
const event = await Event.findByPk(e.eventId, { include: [User, Place, Tag] })
if (!event.place) return
const notification = await Notification.findByPk(e.notificationId)
try {
await sendNotification(notification, event, e)
e.status = 'sent'
return e.save()
} catch (err) {
console.error(err)
e.status = 'error'
return e.save()
}
})
return Promise.all(promises)
}
let interval
function startLoop(seconds) {
console.error('starting notifier loop')
interval = setInterval(notify, seconds * 1000)
}
function stopLoop() {
stopInterval(interval)
}
module.exports = { startLoop, stopLoop }

11
static/README.md Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
static/gancio.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

10
store/README.md Normal file
View 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).

143
store/index.js Normal file
View File

@@ -0,0 +1,143 @@
import moment from 'dayjs'
import intersection from 'lodash/intersection'
import map from 'lodash/map'
export const state = () => ({
config: {},
events: [],
tags: [],
places: [],
settings: {},
filters: {
tags: [],
places: [],
show_past_events: false,
show_recurrent_events: false,
show_pinned_event: false
}
})
export const getters = {
// filter current + future events only
// plus, filter matches search tag/place
filteredEvents: (state) => {
let events = state.events
// TOFIX: use lodash
if (state.filters.tags.length || state.filters.places.length) {
events = events.filter((e) => {
if (state.filters.tags.length) {
const m = intersection(e.tags.map(t => t.tag), state.filters.tags)
if (m.length > 0) return true
}
if (state.filters.places.length) {
if (state.filters.places.find(p => p === e.place.id)) {
return true
}
}
return 0
})
}
if (!state.show_past_events) {
events = events.filter(e => !e.past)
}
let lastDay = null
events = map(events, e => {
const currentDay = moment(e.start_datetime).date()
e.newDay = (!lastDay || lastDay !== currentDay) && currentDay
lastDay = currentDay
return e
})
return events
}
}
export const mutations = {
setEvents(state, events) {
// set a `past` flag
state.events = events.map((e) => {
const end_datetime = e.end_datetime || moment(e.start_datetime).add('3', 'hour')
const past = (moment().diff(end_datetime, 'minutes') > 0)
e.past = !!past
return e
})
},
addEvent(state, event) {
state.events.push(event)
},
updateEvent(state, event) {
state.events = state.events.map((e) => {
if (e.id !== event.id) return e
return event
})
},
delEvent(state, eventId) {
state.events = state.events.filter(ev => {
return ev.id !== eventId
})
},
update(state, { tags, places }) {
state.tags = tags
state.places = places
},
setSearchTags(state, tags) {
state.filters.tags = tags
},
setSearchPlaces(state, places) {
state.filters.places = places
},
showPastEvents(state, show) {
state.show_past_events = show
},
setSettings(state, settings) {
state.settings = settings
},
setConfig(state, config) {
state.config = config
}
}
export const actions = {
async updateEvents({ commit }, page) {
const events = await this.$axios.$get(`/event/${page.month - 1}/${page.year}`)
commit('setEvents', events)
},
async updateMeta({ commit }) {
const { tags, places } = await this.$axios.$get('/event/meta')
commit('update', { tags, places })
},
async addEvent({ commit }, formData) {
const event = await this.$axios.$post('/user/event', formData)
if (event.user) {
commit('addEvent', event)
}
},
async updateEvent({ commit }, formData) {
const event = await this.$axios.$put('/user/event', formData)
if (event.user) {
commit('updateEvent', event)
}
},
delEvent({ commit }, eventId) {
commit('delEvent', eventId)
},
setSearchTags({ commit }, tags) {
commit('setSearchTags', tags)
},
setSearchPlaces({ commit }, places) {
commit('setSearchPlaces', places)
},
showPastEvents({ commit }, show) {
commit('showPastEvents', show)
},
setSettings({ commit }, settings) {
commit('setSettings', settings)
},
setConfig({ commit }, config) {
commit('setConfig', config)
}
}