Merge branch 'master' into gh

This commit is contained in:
lesion 2021-12-07 01:35:18 +01:00
commit 60e9d95ba8
No known key found for this signature in database
GPG key ID: 352918250B012177
160 changed files with 7131 additions and 5420 deletions

3
.gitignore vendored
View file

@ -1,6 +1,9 @@
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)
### Gancio dev configuration ### Gancio dev configuration
*.sqlite
releases
wp-plugin/wpgancio
config/development.json config/development.json
gancio_config.json gancio_config.json
config.json config.json

View file

@ -1,6 +1,100 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
### Unreleased ### 1.2.2 - 7 dic '21
- shiny new gancio-event\[s\] webcomponents => [docs](https://gancio.org/usage/embed)
- new backend plugin system
- improve media focal point selection
- improve non-js experience (load img, use native lazy loading)
- improve user_confirm / recover code flow
- fix task manager exception
- fix db initialization when a custom setup is used, #131
- remove vue-clipboard2 dependency due to [this](https://github.com/euvl/v-clipboard/issues/18) bug and using a [native with fallback mixin instead](./assets/clipboard.js)
- fix a regression to support old CPU, #130
- makes dialog use fullscreen on mobile
- fix Delete AP Actor Action from fediverse when remote Actor is gone
- add `max` param to /events API
### 1.2.1 - 11 nov '21
- fix `Note` remove from fediverse
- AP Actor is now `Application`, was `Person`
- better handling event AP representations
this release is a step forward to improve AP compatibility with other platforms, thanks @tcit
### 1.2.0 - 9 nov '21
- do not overwrite event slug when title is modified to preserve links
- add public cache to events images
- fix baseurl in initial setup configuration
- fix user removal
- load settings during startup and not for each request
- refactoring user custom locale
- published AP event's type is not `Note` anymore but `Event`
### 1.1.1 - 29 ott '21
- fix issue adding event with dueHour resulting in `bad request`
- fix restart during setup
- do not use @nuxt/vuetify module, manually preload vuetify via plugin
- remove deprecated nuxt-express-module and use serverMiddleware directly
### 1.1.0 - 26 ott '21
- a whole new setup via web! fix #126
- new SMTP configuration dialog, fix #115
- re-order general settings in admin panel
- new dark/light theme setting
- move quite all configuration into db
- fix some email recipients
- fix hidden events when not ended
- update translations
- improve install documentation
- add systemd gancio.service
- allow italic and span tags inside editor
- remove moment-timezone, consola, config, inquirer dependencies
- update deps
### 1.0.6 (alpha)
- fix Dockerfile yarn cache issue on update, #123
- fix overflow on event title @homepage
- better import dialog on mobile
- re-add attachment to AP
- fix max event export
- update deps
### 1.0.5 (alpha)
- fix/improve debian install docs
- fix ics export, use new ics package
- use slug url everywhere (rss feed, embedded list)
- use i18n in event confirmation email
- remove lot of deps warning and remove some unused dependencies
- fix show_recurrent in embedded list
- remove old to-ico dep, use png favicon instead
### 1.0.4 (alpha)
- shows a generic img for events without it
### 1.0.3 (alpha)
- 12 hour clock selection, #119
- improve media management
- add alt-text to featured image, fix #106
- add focalPoint support, fix #116
- improve a11y
- improve node v16 compatibility
- fix #122 ? (downgrade prettier)
### 1.0.2 (alpha)
- improve oauth flow UI
- [WordPress plugin](https://wordpress.org/plugins/wpgancio/)
- fix h-event import
- improve error logging (add stack trace to exception)
- choose start date for recurreing events (#120)
- fix user delete from admin
### 1.0.1 (alpha)
- fix AP resource removal
- improve AP resource UI
- fix Docker setup
- update deps
### 1.0 (alpha) ### 1.0 (alpha)
This release is a complete rewrite of frontend UI and many internals, main changes are: This release is a complete rewrite of frontend UI and many internals, main changes are:

11
RELEASE.md Normal file
View file

@ -0,0 +1,11 @@
- change version in package.json
- add changes to CHANGELOG / changelog.md
- yarn build
- yarn pack
- yarn publish
- yarn doc
- git add .
- git ci -m 'v...'
- git tag ...
- git push --tags
-

View file

@ -1,3 +0,0 @@
export default function (to, from, savedPosition) {
return { x: 0, y: 0 }
}

18
assets/clipboard.js Normal file
View file

@ -0,0 +1,18 @@
export default {
methods: {
clipboard (str, msg = 'common.copied') {
try {
navigator.clipboard.writeText(str)
} catch (e) {
const el = document.createElement('textarea')
el.addEventListener('focusin', e => e.stopPropagation())
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
this.$root.$message(msg)
}
}
}

View file

@ -37,6 +37,9 @@ li {
.v-dialog { .v-dialog {
width: 600px; width: 600px;
max-width: 800px; max-width: 800px;
&.v-dialog--fullscreen {
max-width: 100%;
}
} }
.theme--dark.v-list { .theme--dark.v-list {
@ -62,14 +65,14 @@ li {
overflow: hidden; overflow: hidden;
.title { .title {
transition: all .5s; display: -webkit-box;
display: block;
max-height: 3em;
color: white;
overflow: hidden; overflow: hidden;
margin: 0.5rem 1rem 0.5rem 1rem; margin: 0.5rem 1rem 0.5rem 1rem;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
font-size: 1.1em !important; font-size: 1.1em !important;
line-height: 1em !important; line-height: 1.2em !important;
} }
.body { .body {
@ -80,9 +83,9 @@ li {
width: 100%; width: 100%;
max-height: 250px; max-height: 250px;
min-height: 160px; min-height: 160px;
background-color: #222;
object-fit: cover; object-fit: cover;
object-position: top; object-position: top;
aspect-ratio: 1.7778;
} }
.place { .place {
@ -99,10 +102,6 @@ li {
} }
} }
.v-list {
background-color: #333 !important;
}
.vc-past { .vc-past {
opacity: 0.4; opacity: 0.4;
} }
@ -117,3 +116,13 @@ li {
overflow: hidden; overflow: hidden;
display: block; display: block;
} }
.cursorPointer {
cursor: pointer;
}
pre {
white-space: break-spaces;
font-size: 13px;
}

View file

@ -8,6 +8,7 @@
:locale='$i18n.locale' :locale='$i18n.locale'
:attributes='attributes' :attributes='attributes'
transition='fade' transition='fade'
aria-label='Calendar'
is-expanded is-expanded
is-inline is-inline
@dayclick='click') @dayclick='click')

View file

@ -1,6 +1,7 @@
<template lang="pug"> <template lang="pug">
v-dialog(v-model='show' v-dialog(v-model='show'
:fullscreen='$vuetify.breakpoint.xsOnly'
:color='options.color' :color='options.color'
:title='title' :title='title'
:max-width='options.width' :max-width='options.width'
@ -11,8 +12,8 @@
v-card-text(v-show='!!message') {{ message }} v-card-text(v-show='!!message') {{ message }}
v-card-actions v-card-actions
v-spacer v-spacer
v-btn(color='error' @click='cancel') {{$t('common.cancel')}} v-btn(text color='error' @click='cancel') {{$t('common.cancel')}}
v-btn(color='primary' @click='agree') {{$t('common.ok')}} v-btn(text color='primary' @click='agree') {{$t('common.ok')}}
</template> </template>
<script> <script>

View file

@ -1,5 +1,5 @@
<template lang='pug'> <template lang='pug'>
.editor.grey.darken-4(:class='focused') .editor(:class='focused')
.label {{label}} .label {{label}}
editor-menu-bar.menubar.is-hidden(:editor='editor' editor-menu-bar.menubar.is-hidden(:editor='editor'
:keep-in-bounds='true' v-slot='{ commands, isActive, getMarkAttrs, focused }') :keep-in-bounds='true' v-slot='{ commands, isActive, getMarkAttrs, focused }')

View file

@ -1,7 +1,7 @@
<template lang="pug"> <template lang="pug">
v-card.h-event.event.d-flex v-card.h-event.event.d-flex
nuxt-link(:to='`/event/${event.slug || event.id}`') nuxt-link(:to='`/event/${event.slug || event.id}`')
v-img.u-featured.img(:src="`/media/thumb/${event.image_path || 'logo.svg' }`") img.img.u-featured(:src='thumbnail' :alt='alt' loading='lazy' :style="{ 'object-position': thumbnailPosition }")
v-icon.float-right.mr-1(v-if='event.parentId' color='success') mdi-repeat v-icon.float-right.mr-1(v-if='event.parentId' color='success') mdi-repeat
.title.p-name {{event.title}} .title.p-name {{event.title}}
@ -17,17 +17,16 @@
v-menu(offset-y) v-menu(offset-y)
template(v-slot:activator="{on}") template(v-slot:activator="{on}")
v-btn.align-self-end(icon v-on='on' color='primary') v-btn.align-self-end(icon v-on='on' color='primary' alt='more')
v-icon mdi-dots-vertical v-icon mdi-dots-vertical
v-list(dense) v-list(dense)
v-list-item-group v-list-item-group
v-list-item(v-clipboard:success="() => $root.$message('common.copied', { color: 'success' })" v-list-item(@click='clipboard(`${settings.baseurl}/event/${event.slug || event.id}`)')
v-clipboard:copy='`${settings.baseurl}/event/${event.id}`')
v-list-item-icon v-list-item-icon
v-icon mdi-content-copy v-icon mdi-content-copy
v-list-item-content v-list-item-content
v-list-item-title {{$t('common.copy_link')}} v-list-item-title {{$t('common.copy_link')}}
v-list-item(:href='`/api/event/${event.id}.ics`') v-list-item(:href='`/api/event/${event.slug || event.id}.ics`')
v-list-item-icon v-list-item-icon
v-icon mdi-calendar-export v-icon mdi-calendar-export
v-list-item-content v-list-item-content
@ -45,13 +44,34 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import clipboard from '../assets/clipboard'
export default { export default {
props: { props: {
event: { type: Object, default: () => ({}) } event: { type: Object, default: () => ({}) }
}, },
mixins: [clipboard],
computed: { computed: {
...mapState(['settings']), ...mapState(['settings']),
thumbnail () {
let path
if (this.event.media && this.event.media.length) {
path = '/media/thumb/' + this.event.media[0].url
} else {
path = '/noimg.svg'
}
return path
},
alt () {
return this.event.media && this.event.media.length ? this.event.media[0].name : ''
},
thumbnailPosition () {
if (this.event.media && this.event.media.length && this.event.media[0].focalpoint) {
const focalpoint = this.event.media[0].focalpoint
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
}
return 'center center'
},
is_mine () { is_mine () {
if (!this.$auth.user) { if (!this.$auth.user) {
return false return false

View file

@ -2,7 +2,7 @@
v-card v-card
v-card-title(v-text="$t('common.follow_me_title')") v-card-title(v-text="$t('common.follow_me_title')")
v-card-text v-card-text
p(v-html="$t('event.follow_me_description', { title: settings.title, account: `@${settings.instance_name}@${domain}`})") p(v-html="$t('event.follow_me_description', { title: settings.title, account: `@${settings.instance_name}@${settings.hostname}`})")
v-text-field( v-text-field(
:rules="[$validators.required('common.url')]" :rules="[$validators.required('common.url')]"
:loading='loading' :loading='loading'
@ -39,10 +39,6 @@ export default {
computed: { computed: {
...mapState(['settings']), ...mapState(['settings']),
domain () {
const URL = new window.URL(this.settings.baseurl)
return URL.hostname
},
couldGo () { couldGo () {
// check if is mastodon // check if is mastodon
this.get_instance_info(this.instance_hostname) this.get_instance_info(this.instance_hostname)
@ -50,7 +46,7 @@ export default {
}, },
link () { link () {
// check if exists // check if exists
return `https://${this.instance_hostname}/authorize_interaction?uri=${this.settings.instance_name}@${this.domain}` return `https://${this.instance_hostname}/authorize_interaction?uri=${this.settings.instance_name}@${this.settings.hostname}`
} }
}, },
methods: { methods: {
@ -76,7 +72,7 @@ export default {
} }
} }
</script> </script>
<style lang="less"> <style>
.instance_thumb { .instance_thumb {
height: 20px; height: 20px;
} }

View file

@ -1,7 +1,7 @@
<template lang="pug"> <template lang="pug">
v-footer(color='secondary') v-footer(aria-label='Footer')
v-dialog(v-model='showFollowMe' destroy-on-close max-width='700px') v-dialog(v-model='showFollowMe' destroy-on-close max-width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
FollowMe(@close='showFollowMe=false' is-dialog) FollowMe(@close='showFollowMe=false' is-dialog)
v-btn(color='primary' text href='https://gancio.org' target='_blank') Gancio <small>{{settings.version}}</small> v-btn(color='primary' text href='https://gancio.org' target='_blank') Gancio <small>{{settings.version}}</small>
@ -20,7 +20,7 @@
:href='instance.url' :href='instance.url'
two-line) two-line)
v-list-item-avatar v-list-item-avatar
v-img(:src='`${instance.url}/favicon.ico`') v-img(:src='`${instance.url}/logo.png`')
v-list-item-content v-list-item-content
v-list-item-title {{instance.name}} v-list-item-title {{instance.name}}
v-list-item-subtitle {{instance.label}} v-list-item-subtitle {{instance.label}}
@ -41,6 +41,7 @@ export default {
computed: { computed: {
...mapState(['settings']), ...mapState(['settings']),
footerLinks () { footerLinks () {
if (!this.settings || !this.settings.footerLinks) return []
return this.settings.footerLinks.map(link => { return this.settings.footerLinks.map(link => {
if (/^https?:\/\//.test(link.href)) { if (/^https?:\/\//.test(link.href)) {
return { href: link.href, label: link.label } return { href: link.href, label: link.label }

View file

@ -12,7 +12,6 @@ div#list
v-list-item-subtitle <v-icon small color='success' v-if='event.parentId'>mdi-repeat</v-icon> {{event|when}} v-list-item-subtitle <v-icon small color='success' v-if='event.parentId'>mdi-repeat</v-icon> {{event|when}}
span.primary--text.ml-1 @{{event.place.name}} span.primary--text.ml-1 @{{event.place.name}}
v-list-item-title(v-text='event.title') v-list-item-title(v-text='event.title')
//- a.text-body-1(:href='`/event/${event.id}`' target='_blank') {{event.title}}
</template> </template>
<script> <script>

View file

@ -1,5 +1,5 @@
<template lang="pug"> <template lang="pug">
v-app-bar(app) v-app-bar(app aria-label='Menu')
//- logo, title and description //- logo, title and description
v-list-item(:to='$route.name==="index"?"/about":"/"') v-list-item(:to='$route.name==="index"?"/about":"/"')
@ -14,23 +14,23 @@
v-tooltip(bottom) {{$t('common.add_event')}} v-tooltip(bottom) {{$t('common.add_event')}}
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn(v-if='could_add' icon nuxt to='/add' v-on='on') v-btn(v-if='could_add' icon nuxt to='/add' v-on='on' :aria-label='$t("common.add_event")')
v-icon(large color='primary') mdi-plus v-icon(large color='primary') mdi-plus
v-tooltip(bottom) {{$t('common.share')}} v-tooltip(bottom) {{$t('common.share')}}
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn(icon nuxt to='/export' v-on='on') v-btn(icon nuxt to='/export' v-on='on' :aria-label='$t("common.share")')
v-icon mdi-share-variant v-icon mdi-share-variant
v-tooltip(v-if='!$auth.loggedIn' bottom) {{$t('common.login')}} v-tooltip(v-if='!$auth.loggedIn' bottom) {{$t('common.login')}}
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn(icon nuxt to='/login' v-on='on') v-btn(icon nuxt to='/login' v-on='on' :aria-label='$t("common.login")')
v-icon mdi-login v-icon mdi-login
v-menu(v-else v-menu(v-else
offset-y bottom open-on-hover transition="slide-y-transition") offset-y bottom open-on-hover transition="slide-y-transition")
template(v-slot:activator="{ on, attrs }") template(v-slot:activator="{ on, attrs }")
v-btn(icon v-bind='attrs' v-on='on') v-btn(icon v-bind='attrs' v-on='on' aria-label='Menu')
v-icon mdi-dots-vertical v-icon mdi-dots-vertical
v-list v-list
v-list-item(nuxt to='/settings') v-list-item(nuxt to='/settings')
@ -51,15 +51,17 @@
v-list-item-content v-list-item-content
v-list-item-title {{$t('common.logout')}} v-list-item-title {{$t('common.logout')}}
v-btn(icon v-clipboard:copy='feedLink' v-clipboard:success='copyLink') v-btn(icon @click='clipboard(feedLink, "common.feed_url_copied")' aria-label='RSS')
v-icon(color='orange') mdi-rss v-icon(color='orange') mdi-rss
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import clipboard from '../assets/clipboard'
export default { export default {
name: 'Nav', name: 'Nav',
mixins: [clipboard],
computed: { computed: {
...mapState(['filters', 'settings']), ...mapState(['filters', 'settings']),
feedLink () { feedLink () {
@ -83,9 +85,6 @@ export default {
} }
}, },
methods: { methods: {
copyLink () {
this.$root.$message('common.feed_url_copied')
},
logout () { logout () {
this.$root.$message('common.logout_ok') this.$root.$message('common.logout_ok')
this.$auth.logout() this.$auth.logout()

View file

@ -2,7 +2,7 @@
v-container v-container
v-card-title {{$t('common.announcements')}} v-card-title {{$t('common.announcements')}}
v-card-subtitle(v-html="$t('admin.announcement_description')") v-card-subtitle(v-html="$t('admin.announcement_description')")
v-dialog(v-model='dialog' width='800px') v-dialog(v-model='dialog' width='800px' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card v-card
v-card-title {{$t('admin.new_announcement')}} v-card-title {{$t('admin.new_announcement')}}
v-card-text.px-0 v-card-text.px-0

View file

@ -9,7 +9,7 @@
:headers='headers') :headers='headers')
template(v-slot:item.actions='{ item }') template(v-slot:item.actions='{ item }')
v-btn(text small @click='confirm(item)' color='success') {{$t('common.confirm')}} v-btn(text small @click='confirm(item)' color='success') {{$t('common.confirm')}}
v-btn(text small :to='`/event/${item.id}`' color='success') {{$t('common.preview')}} v-btn(text small :to='`/event/${item.slug || item.id}`' color='success') {{$t('common.preview')}}
v-btn(text small :to='`/add/${item.id}`' color='warning') {{$t('common.edit')}} v-btn(text small :to='`/add/${item.id}`' color='warning') {{$t('common.edit')}}
v-btn(text small @click='remove(item)' v-btn(text small @click='remove(item)'
color='error') {{$t('common.delete')}} color='error') {{$t('common.delete')}}

View file

@ -40,7 +40,7 @@
@blur='save("instance_place", instance_place)' @blur='save("instance_place", instance_place)'
) )
v-dialog(v-model='dialogAddInstance' width="500px") v-dialog(v-model='dialogAddInstance' width='500px' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card v-card
v-card-title {{$t('admin.add_trusted_instance')}} v-card-title {{$t('admin.add_trusted_instance')}}
v-card-text v-card-text

View file

@ -13,8 +13,7 @@
dense :headers='instancesHeader' dense :headers='instancesHeader'
@click:row='instanceSelected') @click:row='instanceSelected')
template(v-slot:item.blocked="{ item }") template(v-slot:item.blocked="{ item }")
v-icon(v-if='item.blocked') mdi-checkbox-intermediate v-icon(@click='toggleBlock(item)') {{item.blocked ? 'mdi-checkbox-intermediate' : 'mdi-checkbox-blank-outline'}}
v-icon(v-else) mdi-checkbox-blank-outline
v-col(:span='11') v-col(:span='11')
span {{$t('common.users')}} span {{$t('common.users')}}
@ -24,49 +23,39 @@
:search='usersFilter' :search='usersFilter'
:hide-default-footer='users.length<5' :hide-default-footer='users.length<5'
dense :headers='usersHeader') dense :headers='usersHeader')
//- template(v-slot:item.username="{item}") template(v-slot:item.blocked="{ item }")
//- a(:href='item.ap_id') {{item.object.preferredUsername}} v-icon(@click='toggleUserBlock(item)') {{item.blocked?'mdi-checkbox-intermediate':'mdi-checkbox-blank-outline'}}
//- el-table-column(:label="$t('common.user')" width='150')
//- template(slot-scope='data')
//- span(slot='reference')
//- a(:href='data.row.object.id' target='_blank') {{data.row.object.name}}
//- small ({{data.row.object.preferredUsername}})
//- el-table-column(:label="$t('common.resources')" width='90')
//- template(slot-scope='data')
//- span {{data.row.resources.length}}
//- el-table-column(:label="$t('common.actions')" width='200')
//- template(slot-scope='data')
//- el-button-group
//- el-button(size='mini'
//- :type='data.row.blocked?"danger":"warning"'
//- @click='toggleUserBlock(data.row)') {{data.row.blocked?$t('admin.unblock'):$t('admin.block')}}
div div
v-card-title {{$t('common.resources')}} v-card-title {{$t('common.resources')}}
v-data-table(:items='resources' v-data-table(:items='resources' dense
:headers='resourcesHeader'
:hide-default-footer='resources.length<10' :hide-default-footer='resources.length<10'
) :items-per-page='10')
//- el-table-column(:label="$t('common.event')") template(v-slot:item.content='{ item }')
//- template(slot-scope='data') span(v-html='item.data.content')
//- span {{data.row.event}} template(v-slot:item.user='{ item }')
//- el-table-column(:label="$t('common.resources')") span {{item.ap_user.preferredUsername}}
//- template(slot-scope='data') template(v-slot:item.event='{ item }')
//- span(:class='{disabled: data.row.hidden}' v-html='data.row.data.content') span {{item.event.title}}
//- el-table-column(:label="$t('common.user')" width='200') template(v-slot:item.actions='{ item }')
//- template(slot-scope='data') v-menu(offset-y)
//- span(:class='{disabled: data.row.hidden}' v-html='data.row.data.actor') template(v-slot:activator="{ on }")
//- el-table-column(:label="$t('common.actions')" width="150") v-btn.mr-2(v-on='on' color='primary' small icon)
//- template(slot-scope='data') v-icon mdi-dots-vertical
//- el-dropdown v-list
//- el-button(type="primary" icon="el-icon-arrow-down" size='mini') {{$t('common.moderation')}} v-list-item(v-if='!item.hidden' @click='hideResource(item, true)')
//- el-dropdown-menu(slot='dropdown') v-list-item-title <v-icon left>mdi-eye-off</v-icon> {{$t('admin.hide_resource')}}
//- el-dropdown-item(v-if='!data.row.hidden' icon='el-icon-remove' @click.native='hideResource(data.row, true)') {{$t('admin.hide_resource')}} v-list-item(v-else @click='hideResource(item, false)')
//- el-dropdown-item(v-else icon='el-icon-success' @click.native='hideResource(data.row, false)') {{$t('admin.show_resource')}} v-list-item-title <v-icon left>mdi-eye</v-icon> {{$t('admin.show_resource')}}
//- el-dropdown-item(icon='el-icon-delete' @click.native='deleteResource(data.row)') {{$t('admin.delete_resource')}} v-list-item(@click='deleteResource(item)')
//- el-dropdown-item(icon='el-icon-lock' @click.native='toggleUserBlock(data.row.ap_user)') {{$t('admin.block_user')}} v-list-item-title <v-icon left>mdi-delete</v-icon> {{$t('admin.delete_resource')}}
//- v-list-item(@click='toggleUserBlock(item.ap_user)')
//- v-list-item-title <v-icon left>mdi-lock</v-icon> {{$t('admin.block_user')}}
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex' import { mapState, mapActions } from 'vuex'
import get from 'lodash/get'
export default { export default {
name: 'Moderation', name: 'Moderation',
@ -76,7 +65,8 @@ export default {
resources: [], resources: [],
users: [], users: [],
usersHeader: [ usersHeader: [
{ value: 'object.preferredUsername', text: 'Name' } { value: 'object.preferredUsername', text: 'Name' },
{ value: 'blocked', text: 'Blocked' }
], ],
instancesHeader: [ instancesHeader: [
{ value: 'domain', text: 'Domain' }, { value: 'domain', text: 'Domain' },
@ -85,44 +75,24 @@ export default {
{ value: 'users', text: 'known users' } { value: 'users', text: 'known users' }
], ],
resourcesHeader: [ resourcesHeader: [
{ value: '', text: '' } { value: 'created', text: 'Created' },
{ value: 'event', text: 'Event' },
{ value: 'user', text: 'user' },
{ value: 'content', text: 'Content' },
{ value: 'actions', text: 'Actions' }
], ],
usersFilter: '', usersFilter: '',
instancesFilter: '' instancesFilter: ''
} }
}, },
computed: { computed: mapState(['settings']),
...mapState(['settings'])
// paginatedResources () {
// return this.resources.slice((this.resourcePage - 1) * this.perPage,
// this.resourcePage * this.perPage)
// },
// paginatedInstances () {
// return this.filteredInstances.slice((this.instancePage - 1) * this.perPage,
// this.instancePage * this.perPage)
// },
// filteredUsers () {
// if (!this.usersFilter) { return this.users }
// const usersFilter = this.usersFilter.toLowerCase()
// return this.users.filter(user => user.name.includes(usersFilter) || user.preferredName.includes(usersFilter))
// },
// filteredInstances () {
// if (!this.instancesFilter) { return this.instances }
// const instancesFilter = this.instancesFilter.toLowerCase()
// return this.instances.filter(instance =>
// (instance.name && instance.name.includes(instancesFilter)) ||
// (instance.domain && instance.domain.includes(instancesFilter))
// )
// },
// paginatedSelectedUsers () {
// return this.filteredUsers.slice((this.userPage - 1) * this.perPage,
// this.userPage * this.perPage)
// }
},
async mounted () { async mounted () {
this.instances = await this.$axios.$get('/instances') this.instances = await this.$axios.$get('/instances')
if (!this.instances.length) {
return
}
this.users = await this.$axios.$get(`/instances/${this.instances[0].domain}`)
this.resources = await this.$axios.$get('/resources') this.resources = await this.$axios.$get('/resources')
// this.users = await this.$axios.$get('/users')
}, },
methods: { methods: {
...mapActions(['setSetting']), ...mapActions(['setSetting']),
@ -133,6 +103,7 @@ export default {
}, },
async instanceSelected (instance) { async instanceSelected (instance) {
this.users = await this.$axios.$get(`/instances/${instance.domain}`) this.users = await this.$axios.$get(`/instances/${instance.domain}`)
this.resources = await this.$axios.$get('/resources', { filters: { instance: instance.domain } })
}, },
async hideResource (resource, hidden) { async hideResource (resource, hidden) {
await this.$axios.$put(`/resources/${resource.id}`, { hidden }) await this.$axios.$put(`/resources/${resource.id}`, { hidden })
@ -140,7 +111,7 @@ export default {
}, },
async toggleUserBlock (ap_user) { async toggleUserBlock (ap_user) {
if (!ap_user.blocked) { if (!ap_user.blocked) {
const ret = await this.$root.$confirm('admin.user_block_confirm') const ret = await this.$root.$confirm('admin.user_block_confirm', { user: get(ap_user, 'object.preferredUsername', ap_user.preferredUsername) })
if (!ret) { return } if (!ret) { return }
} }
await this.$axios.post('/instances/toggle_user_block', { ap_id: ap_user.ap_id }) await this.$axios.post('/instances/toggle_user_block', { ap_id: ap_user.ap_id })
@ -153,6 +124,10 @@ export default {
this.resources = this.resources.filter(r => r.id !== resource.id) this.resources = this.resources.filter(r => r.id !== resource.id)
}, },
async toggleBlock (instance) { async toggleBlock (instance) {
if (!instance.blocked) {
const ret = await this.$root.$confirm('admin.instance_block_confirm', { instance: instance.domain })
if (!ret) { return }
}
await this.$axios.post('/instances/toggle_block', { instance: instance.domain, blocked: !instance.blocked }) await this.$axios.post('/instances/toggle_block', { instance: instance.domain, blocked: !instance.blocked })
instance.blocked = !instance.blocked instance.blocked = !instance.blocked
} }

View file

@ -3,7 +3,7 @@
v-card-title {{$t('common.places')}} v-card-title {{$t('common.places')}}
v-card-subtitle(v-html="$t('admin.place_description')") v-card-subtitle(v-html="$t('admin.place_description')")
v-dialog(v-model='dialog' width='600') v-dialog(v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card(color='secondary') v-card(color='secondary')
v-card-title {{$t('admin.edit_place')}} v-card-title {{$t('admin.edit_place')}}
v-card-text v-card-text

77
components/admin/SMTP.vue Normal file
View file

@ -0,0 +1,77 @@
<template lang="pug">
v-card
v-card-title SMTP Email configuration
v-card-text
p(v-html="$t('admin.smtp_description')")
v-form(v-model='isValid')
v-text-field(v-model='admin_email'
@blur="save('admin_email', admin_email )"
:label="$t('admin.admin_email')"
:rules="$validators.email")
v-text-field(v-model='smtp.host'
:label="$t('admin.smtp_hostname')"
:rules="[$validators.required('admin.smtp_hostname')]")
v-text-field(v-model='smtp.auth.user'
:label="$t('common.user')"
:rules="[$validators.required('common.user')]")
v-text-field(v-model='smtp.auth.pass'
:label="$t('common.password')"
:rules="[$validators.required('common.password')]"
type='password')
v-card-actions
v-spacer
v-btn(color='primary' @click='testSMTP' :loading='loading' :disabled='loading || !isValid') {{$t('admin.smtp_test_button')}}
v-btn(color='warning' @click="done") {{$t("common.ok")}}
</template>
<script>
import { mapActions, mapState } from 'vuex'
export default {
data ({ $store }) {
const smtp = { host: '', auth: { user: '', pass: '' } }
if ($store.state.settings.smtp) {
smtp.host = $store.state.settings.smtp.host
if ($store.state.settings.smtp.auth) {
smtp.auth.user = $store.state.settings.smtp.auth.user
smtp.auth.pass = $store.state.settings.smtp.auth.pass
}
}
return {
isValid: false,
loading: false,
smtp,
admin_email: $store.state.settings.admin_email || ''
}
},
computed: mapState(['settings']),
methods: {
...mapActions(['setSetting']),
async testSMTP () {
this.loading = true
try {
this.setSetting({ key: 'smtp', value: this.smtp })
await this.$axios.$post('/settings/smtp', { smtp: this.smtp })
this.$root.$message(this.$t('admin.smtp_test_success', { admin_email: this.admin_email }), { color: 'success' })
} catch (e) {
console.error(e)
this.$root.$message(e.response && e.response.data, { color: 'error' })
}
this.loading = false
},
save (key, value) {
if (this.settings[key] !== value) {
this.setSetting({ key, value })
}
},
done () {
this.setSetting({ key: 'smtp', value: JSON.parse(JSON.stringify(this.smtp)) })
this.$emit('close')
},
}
}
</script>

View file

@ -3,19 +3,25 @@
v-card-title {{$t('common.settings')}} v-card-title {{$t('common.settings')}}
v-card-text v-card-text
v-text-field(v-model='title'
:label="$t('common.title')"
:hint="$t('admin.title_description')"
@blur='save("title", title)'
persistent-hint)
v-text-field.mt-5(v-model='description'
:label="$t('common.description')"
:hint="$t('admin.description_description')"
persistent-hint
@blur='save("description", description)')
//- select timezone //- select timezone
v-autocomplete(v-model='instance_timezone' v-autocomplete.mt-5(v-model='instance_timezone'
:label="$t('admin.select_instance_timezone')" :label="$t('admin.select_instance_timezone')"
:hint="$t('admin.instance_timezone_description')" :hint="$t('admin.instance_timezone_description')"
:items="filteredTimezones" :items="filteredTimezones"
persistent-hint persistent-hint
item-text='value'
item-value='value'
placeholder='Timezone, type to search') placeholder='Timezone, type to search')
template(v-slot:item='{ item }')
v-list-item-content
v-list-item-title {{item.value}}
v-list-item-subtitle {{item.offset}}
v-select.mt-5( v-select.mt-5(
v-model='instance_locale' v-model='instance_locale'
@ -25,19 +31,6 @@
:items='locales' :items='locales'
) )
v-text-field.mt-5(v-model='title'
:label="$t('common.title')"
:hint="$t('admin.title_description')"
@blur='save("title", title)'
persistent-hint
)
v-text-field.mt-5(v-model='description'
:label="$t('common.description')"
:hint="$t('admin.description_description')"
persistent-hint
@blur='save("description", description)')
v-switch.mt-4(v-model='allow_registration' v-switch.mt-4(v-model='allow_registration'
inset inset
:label="$t('admin.allow_registration_description')") :label="$t('admin.allow_registration_description')")
@ -55,24 +48,43 @@
inset inset
:label="$t('admin.recurrent_event_visible')") :label="$t('admin.recurrent_event_visible')")
v-dialog(v-model='showSMTP' destroy-on-close max-width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
SMTP(@close='showSMTP = false')
v-card-actions
v-btn(text @click='showSMTP=true')
<v-icon v-if='showSMTPAlert' color='error'>mdi-alert</v-icon> {{$t('admin.show_smtp_setup')}}
v-btn(text @click='$emit("complete")' color='primary' v-if='setup') {{$t('common.next')}}
v-icon mdi-arrow-right
</template> </template>
<script> <script>
import SMTP from './SMTP.vue'
import { mapActions, mapState } from 'vuex' import { mapActions, mapState } from 'vuex'
import moment from 'moment-timezone' import moment from 'dayjs'
import _ from 'lodash' import tzNames from './tz.json'
import locales from '../../locales/esm' import locales from '../../locales/esm'
export default { export default {
props: {
setup: { type: Boolean, default: false }
},
components: { SMTP },
name: 'Settings', name: 'Settings',
data ({ $store }) { data ({ $store }) {
return { return {
title: $store.state.settings.title, title: $store.state.settings.title,
description: $store.state.settings.description, description: $store.state.settings.description,
locales: Object.keys(locales).map(locale => ({ value: locale, text: locales[locale] })) locales: Object.keys(locales).map(locale => ({ value: locale, text: locales[locale] })),
showSMTP: false,
} }
}, },
computed: { computed: {
...mapState(['settings']), ...mapState(['settings']),
showSMTPAlert () {
return !this.setup && (!this.settings.admin_email || !this.settings.smtp || !this.settings.smtp.host || !this.settings.smtp.user)
},
instance_locale: { instance_locale: {
get () { return this.settings.instance_locale }, get () { return this.settings.instance_locale },
set (value) { this.setSetting({ key: 'instance_locale', value }) } set (value) { this.setSetting({ key: 'instance_locale', value }) }
@ -99,11 +111,8 @@ export default {
}, },
filteredTimezones () { filteredTimezones () {
const current_timezone = moment.tz.guess() const current_timezone = moment.tz.guess()
const ret = _(moment.tz.names()) tzNames.unshift(current_timezone)
.unshift(current_timezone) return tzNames
.map(tz => ({ value: tz, offset: moment().tz(tz).format('z Z') }))
.value()
return ret
} }
}, },
methods: { methods: {

View file

@ -9,12 +9,12 @@
accept='image/*') accept='image/*')
template(slot='append-outer') template(slot='append-outer')
v-btn(color='warning' text @click='resetLogo') <v-icon>mdi-restore</v-icon> {{$t('common.reset')}} v-btn(color='warning' text @click='resetLogo') <v-icon>mdi-restore</v-icon> {{$t('common.reset')}}
v-img(:src='`${settings.baseurl}/favicon.ico?${logoKey}`' v-img(:src='`${settings.baseurl}/logo.png?${logoKey}`'
max-width="100px" max-height="80px" contain) max-width="60px" max-height="60px" contain)
//- v-switch.mt-5(v-model='is_dark' v-switch.mt-5(v-model='is_dark'
//- inset inset
//- :label="$t('admin.is_dark')") :label="$t('admin.is_dark')")
//- TODO choose theme colors //- TODO choose theme colors
//- v-row //- v-row
@ -32,7 +32,7 @@
//- v-on='on') {{i}} //- v-on='on') {{i}}
//- v-color-picker(light @update:color='c => updateColor(i, c)') //- v-color-picker(light @update:color='c => updateColor(i, c)')
v-dialog(v-model='linkModal' width='500') v-dialog(v-model='linkModal' width='500' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card v-card
v-card-title {{$t('admin.footer_links')}} v-card-title {{$t('admin.footer_links')}}
v-card-text v-card-text
@ -52,17 +52,16 @@
v-card-text v-card-text
v-btn(color='primary' text @click='openLinkModal') <v-icon>mdi-plus</v-icon> {{$t('admin.add_link')}} v-btn(color='primary' text @click='openLinkModal') <v-icon>mdi-plus</v-icon> {{$t('admin.add_link')}}
v-btn(color='warning' text @click='reset') <v-icon>mdi-restore</v-icon> {{$t('common.reset')}} v-btn(color='warning' text @click='reset') <v-icon>mdi-restore</v-icon> {{$t('common.reset')}}
v-list.mt-1(two-line subheader) v-card
v-list-item(v-for='link in settings.footerLinks' v-list.mt-1(two-line subheader)
:key='`${link.label}`' @click='editFooterLink(link)') v-list-item(v-for='link in settings.footerLinks'
v-list-item-content :key='`${link.label}`' @click='editFooterLink(link)')
v-list-item-title {{link.label}} v-list-item-content
v-list-item-subtitle {{link.href}} v-list-item-title {{link.label}}
v-list-item-action v-list-item-subtitle {{link.href}}
//- v-btn.float-right(icon color='accent' @click='editFooterLink(link)') v-list-item-action
//- v-icon mdi-pencil v-btn(icon color='error' @click.stop='removeFooterLink(link)')
v-btn(icon color='error' @click.stop='removeFooterLink(link)') v-icon mdi-delete-forever
v-icon mdi-delete-forever
</template> </template>
<script> <script>

View file

@ -10,7 +10,7 @@
v-btn(color='primary' text @click='newUserDialog = true') <v-icon>mdi-plus</v-icon> {{$t('common.new_user')}} v-btn(color='primary' text @click='newUserDialog = true') <v-icon>mdi-plus</v-icon> {{$t('common.new_user')}}
//- ADD NEW USER //- ADD NEW USER
v-dialog(v-model='newUserDialog' :fullscreen="$vuetify.breakpoint.xsOnly") v-dialog(v-model='newUserDialog' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card(color='secondary') v-card(color='secondary')
v-card-title {{$t('common.new_user')}} v-card-title {{$t('common.new_user')}}
@ -37,6 +37,7 @@
v-icon(v-if='item.is_active' color='success') mdi-check v-icon(v-if='item.is_active' color='success') mdi-check
v-icon(v-else color='warning') mdi-close v-icon(v-else color='warning') mdi-close
template(v-slot:item.actions='{item}') template(v-slot:item.actions='{item}')
v-btn(v-if='item.recover_code' text small :to='`/user_confirm/${item.recover_code}`') {{$t('common.confirm')}}
v-btn(text small @click='toggle(item)' v-btn(text small @click='toggle(item)'
:color='item.is_active?"warning":"success"') {{item.is_active?$t('common.disable'):$t('common.enable')}} :color='item.is_active?"warning":"success"') {{item.is_active?$t('common.disable'):$t('common.enable')}}
v-btn(text small @click='toggleAdmin(item)' v-btn(text small @click='toggleAdmin(item)'
@ -76,9 +77,16 @@ export default {
async deleteUser (user) { async deleteUser (user) {
const ret = await this.$root.$confirm('admin.delete_user_confirm', { user: user.email }) const ret = await this.$root.$confirm('admin.delete_user_confirm', { user: user.email })
if (!ret) { return } if (!ret) { return }
await this.$axios.delete(`/user/${user.id}`) try {
this.$root.$message('admin.user_remove_ok') this.loading = true
this.users_ = this.users_.filter(u => u.id !== user.id) await this.$axios.$delete(`/user/${user.id}`)
this.$root.$message('admin.user_remove_ok')
this.$emit('update')
} catch (e) {
const err = get(e, 'response.data.errors[0].message', e)
this.$root.$message(this.$t(err), { color: 'error' })
this.loading = false
}
}, },
async toggle (user) { async toggle (user) {
if (user.is_active) { if (user.is_active) {

595
components/admin/tz.json Normal file
View file

@ -0,0 +1,595 @@
[
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Asmara",
"Africa/Asmera",
"Africa/Bamako",
"Africa/Bangui",
"Africa/Banjul",
"Africa/Bissau",
"Africa/Blantyre",
"Africa/Brazzaville",
"Africa/Bujumbura",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/Conakry",
"Africa/Dakar",
"Africa/Dar_es_Salaam",
"Africa/Djibouti",
"Africa/Douala",
"Africa/El_Aaiun",
"Africa/Freetown",
"Africa/Gaborone",
"Africa/Harare",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Kampala",
"Africa/Khartoum",
"Africa/Kigali",
"Africa/Kinshasa",
"Africa/Lagos",
"Africa/Libreville",
"Africa/Lome",
"Africa/Luanda",
"Africa/Lubumbashi",
"Africa/Lusaka",
"Africa/Malabo",
"Africa/Maputo",
"Africa/Maseru",
"Africa/Mbabane",
"Africa/Mogadishu",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Niamey",
"Africa/Nouakchott",
"Africa/Ouagadougou",
"Africa/Porto-Novo",
"Africa/Sao_Tome",
"Africa/Timbuktu",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Anguilla",
"America/Antigua",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Argentina/Catamarca",
"America/Argentina/ComodRivadavia",
"America/Argentina/Cordoba",
"America/Argentina/Jujuy",
"America/Argentina/La_Rioja",
"America/Argentina/Mendoza",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Aruba",
"America/Asuncion",
"America/Atikokan",
"America/Atka",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Blanc-Sablon",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Buenos_Aires",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Catamarca",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Coral_Harbour",
"America/Cordoba",
"America/Costa_Rica",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Ensenada",
"America/Fort_Nelson",
"America/Fort_Wayne",
"America/Fortaleza",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Indianapolis",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Indianapolis",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Jujuy",
"America/Juneau",
"America/Kentucky/Louisville",
"America/Kentucky/Monticello",
"America/Knox_IN",
"America/Kralendijk",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Louisville",
"America/Lower_Princes",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Mendoza",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/Montreal",
"America/Montserrat",
"America/Nassau",
"America/New_York",
"America/Nipigon",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Nuuk",
"America/Ojinaga",
"America/Panama",
"America/Pangnirtung",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Port_of_Spain",
"America/Porto_Acre",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rainy_River",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Rosario",
"America/Santa_Isabel",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Shiprock",
"America/Sitka",
"America/St_Barthelemy",
"America/St_Johns",
"America/St_Kitts",
"America/St_Lucia",
"America/St_Thomas",
"America/St_Vincent",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Thunder_Bay",
"America/Tijuana",
"America/Toronto",
"America/Tortola",
"America/Vancouver",
"America/Virgin",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"America/Yellowknife",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/DumontDUrville",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/McMurdo",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/South_Pole",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"Arctic/Longyearbyen",
"Asia/Aden",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Ashkhabad",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Calcutta",
"Asia/Chita",
"Asia/Choibalsan",
"Asia/Chongqing",
"Asia/Chungking",
"Asia/Colombo",
"Asia/Dacca",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Harbin",
"Asia/Hebron",
"Asia/Ho_Chi_Minh",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Istanbul",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Kashgar",
"Asia/Kathmandu",
"Asia/Katmandu",
"Asia/Khandyga",
"Asia/Kolkata",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macao",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Muscat",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Phnom_Penh",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Rangoon",
"Asia/Riyadh",
"Asia/Saigon",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Tel_Aviv",
"Asia/Thimbu",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ujung_Pandang",
"Asia/Ulaanbaatar",
"Asia/Ulan_Bator",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yangon",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faeroe",
"Atlantic/Faroe",
"Atlantic/Jan_Mayen",
"Atlantic/Madeira",
"Atlantic/Reykjavik",
"Atlantic/South_Georgia",
"Atlantic/St_Helena",
"Atlantic/Stanley",
"Australia/ACT",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Canberra",
"Australia/Currie",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/LHI",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/NSW",
"Australia/North",
"Australia/Perth",
"Australia/Queensland",
"Australia/South",
"Australia/Sydney",
"Australia/Tasmania",
"Australia/Victoria",
"Australia/West",
"Australia/Yancowinna",
"Brazil/Acre",
"Brazil/DeNoronha",
"Brazil/East",
"Brazil/West",
"CET",
"CST6CDT",
"Canada/Atlantic",
"Canada/Central",
"Canada/Eastern",
"Canada/Mountain",
"Canada/Newfoundland",
"Canada/Pacific",
"Canada/Saskatchewan",
"Canada/Yukon",
"Chile/Continental",
"Chile/EasterIsland",
"Cuba",
"EET",
"EST",
"EST5EDT",
"Egypt",
"Eire",
"Etc/GMT",
"Etc/GMT+0",
"Etc/GMT+1",
"Etc/GMT+10",
"Etc/GMT+11",
"Etc/GMT+12",
"Etc/GMT+2",
"Etc/GMT+3",
"Etc/GMT+4",
"Etc/GMT+5",
"Etc/GMT+6",
"Etc/GMT+7",
"Etc/GMT+8",
"Etc/GMT+9",
"Etc/GMT-0",
"Etc/GMT-1",
"Etc/GMT-10",
"Etc/GMT-11",
"Etc/GMT-12",
"Etc/GMT-13",
"Etc/GMT-14",
"Etc/GMT-2",
"Etc/GMT-3",
"Etc/GMT-4",
"Etc/GMT-5",
"Etc/GMT-6",
"Etc/GMT-7",
"Etc/GMT-8",
"Etc/GMT-9",
"Etc/GMT0",
"Etc/Greenwich",
"Etc/UCT",
"Etc/UTC",
"Etc/Universal",
"Etc/Zulu",
"Europe/Amsterdam",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belfast",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Chisinau",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Guernsey",
"Europe/Helsinki",
"Europe/Isle_of_Man",
"Europe/Istanbul",
"Europe/Jersey",
"Europe/Kaliningrad",
"Europe/Kiev",
"Europe/Kirov",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/London",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Mariehamn",
"Europe/Minsk",
"Europe/Monaco",
"Europe/Moscow",
"Europe/Nicosia",
"Europe/Oslo",
"Europe/Paris",
"Europe/Podgorica",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/San_Marino",
"Europe/Sarajevo",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Skopje",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Tiraspol",
"Europe/Ulyanovsk",
"Europe/Uzhgorod",
"Europe/Vaduz",
"Europe/Vatican",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zagreb",
"Europe/Zaporozhye",
"Europe/Zurich",
"GB",
"GB-Eire",
"GMT",
"GMT+0",
"GMT-0",
"GMT0",
"Greenwich",
"HST",
"Hongkong",
"Iceland",
"Indian/Antananarivo",
"Indian/Chagos",
"Indian/Christmas",
"Indian/Cocos",
"Indian/Comoro",
"Indian/Kerguelen",
"Indian/Mahe",
"Indian/Maldives",
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Iran",
"Israel",
"Jamaica",
"Japan",
"Kwajalein",
"Libya",
"MET",
"MST",
"MST7MDT",
"Mexico/BajaNorte",
"Mexico/BajaSur",
"Mexico/General",
"NZ",
"NZ-CHAT",
"Navajo",
"PRC",
"PST8PDT",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Chuuk",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Johnston",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Pohnpei",
"Pacific/Ponape",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Saipan",
"Pacific/Samoa",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Pacific/Truk",
"Pacific/Wake",
"Pacific/Wallis",
"Pacific/Yap",
"Poland",
"Portugal",
"ROC",
"ROK",
"Singapore",
"Turkey",
"UCT",
"US/Alaska",
"US/Aleutian",
"US/Arizona",
"US/Central",
"US/East-Indiana",
"US/Eastern",
"US/Hawaii",
"US/Indiana-Starke",
"US/Michigan",
"US/Mountain",
"US/Pacific",
"US/Samoa",
"UTC",
"Universal",
"W-SU",
"WET",
"Zulu"
]

View file

@ -1,26 +0,0 @@
{
"title": "Gancio",
"description": "A shared agenda for local communities",
"baseurl": "http://localhost:13120",
"server": {
"host": "localhost",
"port": 13120
},
"log_level": "debug",
"log_path": "./logs",
"db": {
"dialect": "sqlite",
"storage": "./db.sqlite",
"logging": false
},
"upload_path": "./",
"smtp": {
"auth": {
"user": "",
"pass": ""
},
"secure": true,
"host": ""
},
"admin_email": "admin"
}

View file

@ -1,10 +0,0 @@
// DO NOT TOUCH THIS FILE
const fs = require('fs')
const config_path = process.env.config_path
let config = {}
if (fs.existsSync(config_path)) {
config = require(config_path)
}
module.exports = config

View file

@ -34,11 +34,9 @@ plugins:
search_enabled: true search_enabled: true
aux_links: aux_links:
"Blog":
- https://blog.gancio.org
"Source": "Source":
- https://framagit.org/les/gancio - https://framagit.org/les/gancio
"Mastodon": "@gancio@mastodon.cisti.org":
- https://mastodon.cisti.org/@gancio - https://mastodon.cisti.org/@gancio
gh_edit_link: true # show or hide edit this page link gh_edit_link: true # show or hide edit this page link

View file

@ -8,6 +8,104 @@ nav_order: 10
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
### 1.2.2 - 7 dic '21
- shiny new gancio-event\[s\] webcomponents => [docs](https://gancio.org/usage/embed)
- new backend plugin system
- improve media focal point selection
- improve non-js experience (load img, use native lazy loading)
- improve user_confirm / recover code flow
- fix task manager exception
- fix db initialization when a custom setup is used, #131
- remove vue-clipboard2 dependency due to [this](https://github.com/euvl/v-clipboard/issues/18) bug and using a [native with fallback mixin instead](./assets/clipboard.js)
- fix a regression to support old CPU, #130
- makes dialog use fullscreen on mobile
- fix Delete AP Actor Action from fediverse when remote Actor is gone
- add `max` param to /events API
### 1.2.1 - 11 nov '21
- fix `Note` remove from fediverse
- AP Actor is now `Application`, was `Person`
- better handling event AP representations
this release is a step forward to improve AP compatibility with other platforms, thanks @tcit
### 1.2.0 - 9 nov '21
- do not overwrite event slug when title is modified to preserve links
- add public cache to events images
- fix baseurl in initial setup configuration
- fix user removal
- load settings during startup and not for each request
- refactoring user custom locale
- published AP event's type is not `Note` anymore but `Event`
### 1.1.1 - 29 ott '21
- fix issue adding event with dueHour resulting in `bad request`
- fix restart during setup
- do not use @nuxt/vuetify module, manually preload vuetify via plugin
- remove deprecated nuxt-express-module and use serverMiddleware directly
### 1.1.0 - 26 ott '21
- a whole new setup via web! fix #126
- new SMTP configuration dialog, fix #115
- re-order general settings in admin panel
- new dark/light theme setting
- move quite all configuration into db
- fix some email recipients
- fix hidden events when not ended
- update translations
- improve install documentation
- add systemd gancio.service
- allow italic and span tags inside editor
- remove moment-timezone, consola, config, inquirer dependencies
- update deps
### 1.0.6 (alpha)
- fix Dockerfile yarn cache issue on update, #123
- fix overflow on event title @homepage
- better import dialog on mobile
- re-add attachment to AP
- fix max event export
- update deps
### 1.0.5 (alpha)
- fix/improve debian install docs
- fix ics export, use new ics package
- use slug url everywhere (rss feed, embedded list)
- use i18n in event confirmation email
- remove lot of deps warning and remove some unused dependencies
- fix show_recurrent in embedded list
- remove old to-ico dep, use png favicon instead
### 1.0.4 (alpha)
- shows a generic img for events without it
### 1.0.3 (alpha)
- 12 hour clock selection, #119
- improve media management
- add alt-text to featured image, fix #106
- add focalPoint support, fix #116
- improve a11y
- improve node v16 compatibility
- fix #122 ? (downgrade prettier)
### 1.0.2 (alpha)
- improve oauth flow UI
- [WordPress plugin](https://wordpress.org/plugins/wpgancio/)
- fix h-event import
- improve error logging (add stack trace to exception)
- choose start date for recurreing events (#120)
- fix user delete from admin
### 1.0.1 (alpha)
- fix AP resource removal
- improve AP resource UI
- fix Docker setup
- update deps
### 1.0 (alpha) ### 1.0 (alpha)
This release is a complete rewrite of frontend UI and many internals, main changes are: This release is a complete rewrite of frontend UI and many internals, main changes are:

View file

@ -7,5 +7,8 @@ nav_order: 9
## Contacts ## Contacts
### :elephant: Mastodon ⇒ [@gancio@mastodon.cisti.org](https://mastodon.cisti.org/@gancio)
- :elephant: Mastodon ⇒ [@gancio@mastodon.cisti.org](https://mastodon.cisti.org/@gancio)
- :email: Email ⇒ [info@cisti.org](mailto:info@cisti.org)
- IRC ⇒ #gancio @ irc.autistici.org (sometimes...)

View file

@ -1,2 +1,7 @@
FROM node:buster FROM node:buster
RUN yarn global add --silent https://gancio.org/latest.tgz 2> /dev/null RUN yarn global remove gancio || true
RUN yarn cache clean
RUN yarn global add --latest --production --silent https://gancio.org/latest.tgz 2> /dev/null
ADD entrypoint.sh /
RUN chmod 755 /entrypoint.sh
ENTRYPOINT [ "/bin/sh", "/entrypoint.sh" ]

4
docs/docker/entrypoint.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
chown -R node:node /home/node
su node -c "$*"

View file

@ -19,12 +19,13 @@ services:
build: . build: .
restart: always restart: always
image: node:buster image: node:buster
user: node
container_name: gancio container_name: gancio
environment: environment:
- PATH=$PATH:/home/node/.yarn/bin - PATH=$PATH:/home/node/.yarn/bin
- GANCIO_DATA=/home/node/data - GANCIO_DATA=/home/node/data
- NODE_ENV=production
command: gancio start --docker command: gancio start --docker
entrypoint: /entrypoint.sh
volumes: volumes:
- ./data:/home/node/data - ./data:/home/node/data
ports: ports:

View file

@ -5,11 +5,12 @@ services:
build: . build: .
restart: always restart: always
image: node:buster image: node:buster
user: node
container_name: gancio container_name: gancio
environment: environment:
- PATH=$PATH:/home/node/.yarn/bin - PATH=$PATH:/home/node/.yarn/bin
- GANCIO_DATA=/home/node/data - GANCIO_DATA=/home/node/data
- NODE_ENV=production
entrypoint: /entrypoint.sh
command: gancio start --docker command: gancio start --docker
volumes: volumes:
- ./data:/home/node/data - ./data:/home/node/data

33
docs/embed.md Normal file
View file

@ -0,0 +1,33 @@
---
layout: default
title: Embed events
permalink: /usage/embed
nav_order: 1
parent: Usage
---
## Embed event
You can embed a list of filtered events or a specific event card in your webpage using a classic old-school `iframe` or a shiny new webcomponent.
### Webcomponents
The webcomponent require a small js to be loaded in your page:
```javascript
<script src='https://demo.gancio.org/gancio-events.es.js'></script>
```
#### embed a single event
> you can copy the code in **event page > Embed > Copy**
<script src='https://demo.gancio.org/gancio-events.es.js'></script>
<gancio-event id=17 baseurl='https://demo.gancio.org'></gancio-event>
```javascript
<gancio-event id=17 baseurl='https://demo.gancio.org'></gancio-event>
```
#### embed event lists
> you can copy the code in **Export > List > Copy**
<gancio-events baseurl='https://demo.gancio.org'></gancio-events>
```javascript
<gancio-event baseurl='https://demo.gancio.org'></gancio-event>
```

View file

@ -5,12 +5,8 @@ permalink: /federation
nav_order: 9 nav_order: 9
--- ---
## Federation ## Federation / ActivityPub
Each instance has only one [AP Actor](https://www.w3.org/TR/activitypub/#actors) that publishes each event. Each instance has only one [AP Actor](https://www.w3.org/TR/activitypub/#actors) of type `Application` named `gancio@instance.tld` that publishes each event.
We are considering the introduction of other “Actor” but they will not be linked to users, rather to places or tags/categories. We are considering the introduction of other `Actor` but they will not be linked to users, rather to places or tags/categories.
There are no personal homes with a timeline of people I follow, everyone has a sort of local timeline of the instance, its an anti filter-bubble feature. There are no personal homes with a timeline of people you follow, everyone has a sort of local timeline of the instance, its an anti filter-bubble feature.
Events are not published with the type `Event` but with type `Note` because we wanted to add the possibility to interact with events from mastodon instances (boost / bookmark and “comments” that we call resources because we dont want it to become a place of debate, but more a place where to keep a historical memory of events, e.g. an audio recording of a talk).
When mastodon will support `Event` object type we will change for sure.

13
docs/gancio.service Normal file
View file

@ -0,0 +1,13 @@
[Unit]
Description=Gancio
After=network.target
[Service]
Type=simple
User=gancio
WorkingDirectory=/opt/gancio
ExecStart=gancio
Restart=always
[Install]
WantedBy=multi-user.target

View file

@ -8,16 +8,43 @@ parent: Install
## Backup ## Backup
The following commands should be valid for every setup (docker/debian/sqlite/postgres) but check your installation directory first. The following commands should be valid for every setup (docker/debian/sqlite/postgres).
This includes database, configuration, custom user locales, logs, images and thumbnails.
1. Move to gancio path
```bash ```bash
cd /opt/gancio/ # or /home/gancio or where your installation is cd /opt/gancio/ # or where your installation is
tar -czf gancio-$(date +%Y-%m-%d-%H%M%S)-backup.tgz \ ```
$(ls -d config.json uploads user_locale db.sqlite postgres data logs 2> /dev/null)
1. Backup PostgreSQL (only required for non-docker PostgreSQL installation)
```bash
sudo -u postgres pg_dump -Fc gancio > gancio.dump
```
1. Archive database, configuration, custom user locales, logs, images and thumbnails
```bash
sudo tar -czf gancio-$(date +%Y-%m-%d-%H%M%S)-backup.tgz \
$(ls -d config.json uploads user_locale db.sqlite gancio.dump postgres data logs 2> /dev/null)
``` ```
> warning "Permission denied"
> `postgres` directory could have different permission or owner, in this case you need to be root or use `sudo` instead.
> info "Automatic backup" > info "Automatic backup"
> To periodically backup your data you should probably use something like [restic](https://restic.net) or [borg](https://www.borgbackup.org/) > To periodically backup your data you should probably use something like [restic](https://restic.net) or [borg](https://www.borgbackup.org/)
## Restore
1. Install a clean gancio
1. Move to gancio path
```bash
cd /opt/gancio/ # or where your installation is
```
1. Extract your backup
```bash
tar xvf gancio-*-backup.tgz
```
1. Restore PostgreSQL database (only required for non-docker PostgreSQL installation)
```
sudo -u postgres createdb gancio
sudo -u postgres pg_restore -d gancio gancio.dump
```

View file

@ -16,22 +16,8 @@ The configuration file shoud be a `.json` or a `.js` file and could be specified
1. TOC 1. TOC
{:toc} {:toc}
- ### Title
The title will be in rss feed, in html head and in emails:
`"title": "Gancio"`
![title](../assets/title.png)
- ### Description
`"description": "a shared agenda for local communities"`
- ### BaseURL
URL where your site will be accessible (include http or https):
`"baseurl": "https://gancio.cisti.org"`
- ### Server - ### Server
This probably support unix socket too :D This probably support unix socket too
```json ```json
"server": { "server": {
@ -52,78 +38,28 @@ DB configuration, look [here](https://sequelize.org/master/class/lib/sequelize.j
Where to save images Where to save images
`"upload_path": "./uploads"` `"upload_path": "./uploads"`
- ### SMTP
SMTP configuration.
Gancio should send emails at following events:
- the admin should receive emails of anon event (if enabled) to confirm them.
- the admin should receive emails of registration request (if enabled) to confirm them.
- an user should receive an email of registration requested.
- an user should receive an email of confirmed registration.
- an user should receive a confirmation email when subscribed directly by admin.
```json
"smtp": {
"auth": {
"user": "",
"pass": ""
},
"secure": true,
"host": ""
}
```
- ### Admin_email
Email of administrator. Note that email from gancio comes from this email and that
the SMTP configuration above should allow to use this address as from.
- ### User locale - ### User locale
Probably you want to modify some text for your specific community, that's Probably you want to modify some text for your specific community, that's
why we thought the `user_locale` configuration: you can specify your version of why we thought the `user_locale` configuration: you can specify your version of
each string of **gancio** making a directory with your locales inside. each string of **gancio** making a directory with your locales inside.
For example, let's say you want to modify the text inside the `/about` For example, let's say you want to modify the text shown during registration:
page:
`mkdir /opt/gancio/user_locale` `mkdir /opt/gancio/user_locale`
put something like this in `/opt/gancio/user_locale/en.js` to override the about in
put something like this in `/opt/gancio/user_locale/en.json` to override the registration description in
english: english:
```js ```json
export default { {
about: 'A new about' "registrer": {
"description": "My new registration page description"
}
} }
``` ```
and then point the `user_locale` configuration to that directory: and then point the `user_locale` configuration to that directory (in your `config.json`):
```json ```json
"user_locale": "/opt/gancio/user_locale" "user_locale": "/opt/gancio/user_locale"
``` ```
Watch [here](https://framagit.org/les/gancio/tree/master/locales) for a Watch [here](https://framagit.org/les/gancio/tree/master/locales) for a
list of strings you can override. list of strings you can override.
<small>:warning: Note that a restart is needed when you change
user_locale's content.</small>
> warning "Restart needed"
## Default settings > Note that a restart is needed when you change user_locale's content.
```json
{
"title": "Gancio",
"description": "A shared agenda for local communities",
"baseurl": "http://localhost:13120",
"server": {
"host": "127.0.0.1",
"port": 13120
},
"db": {
"dialect": "sqlite",
"storage": "./db.sqlite"
},
"upload_path": "./",
"smtp": {
"auth": {
"user": "",
"pass": ""
},
"secure": true,
"host": ""
},
"admin_email": "",
}
```

View file

@ -7,24 +7,24 @@ parent: Install
## Debian installation ## Debian installation
1. Install Node.js & yarn (**from root**) 1. Install dependencies
```bash ```bash
curl -sL https://deb.nodesource.com/setup_16.x | bash - sudo apt install curl gcc g++ make wget libpq-dev
apt-get install -y nodejs ```
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
apt-get update && apt-get install yarn 1. Install Node.js & yarn
```bash
curl -sL https://deb.nodesource.com/setup_16.x | sudo bash -
sudo apt-get install -y nodejs
sudo npm install -g yarn
``` ```
<small>[source](https://github.com/nodesource/distributions/blob/master/README.md)</small> <small>[source](https://github.com/nodesource/distributions/blob/master/README.md)</small>
1. Install Gancio
```bash
yarn global add --silent {{site.url}}{% link /latest.tgz %} 2> /dev/null
```
1. Setup with postgreSQL __(optional as you can choose sqlite)__ 1. Setup with postgreSQL __(optional as you can choose sqlite)__
```bash ```bash
apt-get install postgresql sudo apt-get install postgresql
# Create the database # Create the database
su postgres -c psql su postgres -c psql
postgres=# create database gancio; postgres=# create database gancio;
@ -34,36 +34,38 @@ postgres=# grant all privileges on database gancio to gancio;
1. Create a user to run gancio from 1. Create a user to run gancio from
```bash ```bash
adduser gancio sudo adduser --group --system --shell /bin/false --home /opt/gancio gancio
su gancio ```
1. Install Gancio
```bash
sudo yarn global add --silent {{site.url}}/latest.tgz 2> /dev/null
``` ```
1. Launch interactive setup 1. Setup systemd service and reload systemd
```bash ```bash
gancio setup --config config.json sudo wget http://gancio.org/gancio.service -O /etc/systemd/system/gancio.service
sudo systemctl daemon-reload
sudo systemctl enable gancio
``` ```
1. Start 1. Start gancio service (this should listen on port 13120)
```bash ```bash
gancio start --config config.json sudo systemctl start gancio
``` ```
1. Point your web browser to [http://localhost:13120](http://localhost:13120) or where you selected during setup.
1. [Setup nginx as a proxy]({% link install/nginx.md %}) 1. [Setup nginx as a proxy]({% link install/nginx.md %})
1. To deploy gancio in production you should use something like **[pm2](http://pm2.keymetrics.io/)**: 1. Point your web browser to your domain :tada:
```bash
sudo yarn global add pm2
pm2 start gancio -- --config config.json
# Run this command to run your application as a service and automatically restart after a reboot:
pm2 startup # read the output!
sudo pm2 startup -u gancio
```
## Upgrade ## Upgrade
> warning "Backup your data"
> Backup your data is generally a good thing to do and this is especially true before upgrading.
> Don't be lazy and [backup]({% link install/backup.md %}) your data!
```bash ```bash
sudo yarn global add --silent {{site.url}}{% link /latest.tgz %} 2> /dev/null yarn global remove gancio
sudo service pm2 restart yarn cache clean
yarn global add --silent {{site.url}}/latest.tgz 2> /dev/null
sudo service gancio restart
``` ```

View file

@ -13,28 +13,37 @@ nav_order: 2
## Initial setup ## Initial setup
> info "Clone not needed"
> You do not need to clone the full repo, a `Dockerfile` and a `docker-compose.yml` are enough. - __You must have the following dependencies installed: Docker, Docker Compose and Nginx__
```bash
sudo apt install docker docker-compose nginx
```
or
1. [Install docker](https://docs.docker.com/engine/install/)
1. [Install docker-compose](https://docs.docker.com/compose/install/)
1. [Install nginx](https://nginx.org/en/docs/install.html)
- __Create a directory where everything related to gancio is stored__ - __Create a directory where everything related to gancio is stored__
```bash ```bash
mkdir -p /opt/gancio/data mkdir -p /opt/gancio
cd /opt/gancio cd /opt/gancio
``` ```
## Use sqlite ## Use sqlite
<div class='code-example bg-grey-lt-100' markdown="1"> <div class='code-example bg-grey-lt-100' markdown="1">
1. **Download docker-compose.yml and Dockerfile** 1. **Download docker-compose.yml and Dockerfile**
```bash ```bash
wget {{site.url}}{% link /docker/Dockerfile %} wget {{site.url}}{% link /docker/Dockerfile %}
wget {{site.url}}{% link /docker/entrypoint.sh %}
wget {{site.url}}{% link /docker/sqlite/docker-compose.yml %} wget {{site.url}}{% link /docker/sqlite/docker-compose.yml %}
``` ```
1. Build docker image and launch interactive setup 1. Build docker image
``` ```
docker-compose build docker-compose build
docker-compose run --rm gancio gancio setup --docker --db=sqlite
``` ```
</div> </div>
@ -44,13 +53,13 @@ docker-compose run --rm gancio gancio setup --docker --db=sqlite
1. **Download docker-compose.yml and Dockerfile** 1. **Download docker-compose.yml and Dockerfile**
```bash ```bash
wget {{site.url}}{% link /docker/Dockerfile %} wget {{site.url}}{% link /docker/Dockerfile %}
wget {{site.url}}{% link /docker/entrypoint.sh %}
wget {{site.url}}{% link /docker/postgres/docker-compose.yml %} wget {{site.url}}{% link /docker/postgres/docker-compose.yml %}
``` ```
1. Build docker image and launch interactive setup 1. Build docker image
``` ```
docker-compose build docker-compose build
docker-compose run --rm gancio gancio setup --docker --db=postgres
``` ```
</div> </div>
@ -67,9 +76,9 @@ docker-compose up -d
tail -f data/logs/gancio.log tail -f data/logs/gancio.log
``` ```
1. [Setup nginx as a proxy]({% link install/nginx.md %} 1. [Setup nginx as a proxy]({% link install/nginx.md %})
1. Point your web browser to [http://localhost:13120](http://localhost:13120) or where you specified during setup and enjoy :tada: 1. Point your web browser to your domain :tada:
1. Edit `data/config.json` and restart the container on your needs, see [Configuration]({% link install/configuration.md %}) for more details. 1. Edit `data/config.json` and restart the container on your needs, see [Configuration]({% link install/configuration.md %}) for more details.
@ -86,6 +95,7 @@ tail -f data/logs/gancio.log
> 1. `cd /opt/gancio` > 1. `cd /opt/gancio`
> 1. [Backup your data]({% link install/backup.md %}) > 1. [Backup your data]({% link install/backup.md %})
> 1. Download new `Dockerfile` <br/> `wget {{site.url}}{% link /docker/Dockerfile %}` > 1. Download new `Dockerfile` <br/> `wget {{site.url}}{% link /docker/Dockerfile %}`
> 1. Download new `entrypoint.sh` <br/> `wget {{site.url}}{% link /docker/entrypoint.sh %}`
> 1. Download new `docker-compose.yml` (substitute `sqlite` with `postgres` in case): <br/>`wget {{site.url}}{% link /docker/sqlite/docker-compose.yml %}` > 1. Download new `docker-compose.yml` (substitute `sqlite` with `postgres` in case): <br/>`wget {{site.url}}{% link /docker/sqlite/docker-compose.yml %}`
> 1. Build the new container `docker-compose build` > 1. Build the new container `docker-compose build`
> 1. Extract your backup into `./data` <br/>`mkdir data; tar xvzf gancio-<yourLastBackup>-backup.tgz -C data` > 1. Extract your backup into `./data` <br/>`mkdir data; tar xvzf gancio-<yourLastBackup>-backup.tgz -C data`

View file

@ -6,17 +6,20 @@ has_children: true
nav_order: 3 nav_order: 3
has_toc: false has_toc: false
--- ---
## Install ## Pre-requisites
- a Linux machine with <strong>root access</strong> (a VPS with 500MB of RAM and a cpu should be enough but do not use docker on a small machine :stuck_out_tongue_winking_eye:)
- a domain name or subdomain (eg. gancio.mydomain.org, subpath are not supported)
- an SMTP server to deliver emails
You can install gancio on a cheap VPS (500mb of ram will be enough) ## Install
- [Install on Debian]({% link install/debian.md %}) - [Install on Debian]({% link install/debian.md %})
- [Install using docker]({% link install/docker.md %}) - [Install using docker]({% link install/docker.md %})
### Post installation ### Post installation
- [Setup Nginx as a proxy]({% link install/nginx.md %}) - [Setup a backup]({% link install/backup.md %})
- [Configuration]({% link install/configuration.md %})
- [Backup]({% link install/backup.md %})
If you wanna hack or run the current development release take a look at [Hacking & contribute]({% link dev/dev.md %}) > info "Info"
> If you wanna hack or run the current development release take a look at [Hacking & contribute]({% link dev/dev.md %}).
>

View file

@ -8,69 +8,35 @@ parent: Install
## Nginx proxy configuration ## Nginx proxy configuration
This is the default nginx configuration for gancio, please modify at least the **server_name** and **ssl_certificate**'s path. This is the default nginx configuration for gancio, please modify at least «YOUR_DOMAIN». Note that it does not include HTTPS setup but you can easily use [certbot](https://certbot.eff.org/) for that.
Note that this does not include a cache configuration and that gancio does
not use a cache control at all, if you can help with this task you're - __You should be in the correct directory__
welcome. `/etc/nginx/sites-available`
```nginx ```nginx
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name gancio.cisti.org; server_name <<YOUR_DOMAIN>>;
root /var/www/letsencrypt;
location /.well-known/acme-challenge/ { allow all; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name gancio.cisti.org;
ssl_protocols TLSv1.2;
ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
# Uncomment these lines once you acquire a certificate:
# ssl_certificate /etc/letsencrypt/live/gancio.cisti.org/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/gancio.cisti.org/privkey.pem;
keepalive_timeout 70; keepalive_timeout 70;
sendfile on; sendfile on;
client_max_body_size 80m; client_max_body_size 80m;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
add_header Strict-Transport-Security "max-age=31536000";
location / { location / {
try_files $uri @proxy; try_files $uri @proxy;
} }
location @proxy { location @proxy {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Proxy "";
proxy_pass_header Server;
proxy_pass http://127.0.0.1:13120; proxy_pass http://127.0.0.1:13120;
proxy_buffering on;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
tcp_nodelay on;
} }
} }
```
- __Following this, you should create a link to the file in sites-enabled:__
```bash
ln -s /etc/nginx/sites-available/<your-config> /etc/nginx/sites-enabled/
``` ```

View file

@ -8,7 +8,7 @@ nav_order: 7
- [gancio.cisti.org](https://gancio.cisti.org) (Turin, Italy) - [gancio.cisti.org](https://gancio.cisti.org) (Turin, Italy)
- [lapunta.org](https://lapunta.org) (Florence, Italy) - [lapunta.org](https://lapunta.org) (Florence, Italy)
- [chesefa.org](https://chesefa.org) (Naples, Italy) - [termine.161.social](https://termine.161.social) (Germany)
<small>Do you want your instance to appear here? [Write us]({% link contact.md %}).</small> <small>Do you want your instance to appear here? [Write us]({% link contact.md %}).</small>

Binary file not shown.

View file

@ -6,6 +6,6 @@ nav_order: 1
has_children: true has_children: true
--- ---
# Usage ## Usage
ehmmm, help needed here :smile: feel free to send a PR => [here](https://framagit.org/les/gancio/tree/master/docs) ehmmm, help needed here :smile: feel free to send a PR => [here](https://framagit.org/les/gancio/tree/master/docs)

22
layouts/clean.vue Normal file
View file

@ -0,0 +1,22 @@
<template lang='pug'>
v-app(app)
Snackbar
Confirm
v-main(app)
v-fade-transition(hide-on-leave)
nuxt
</template>
<script>
import Snackbar from '../components/Snackbar'
import Confirm from '../components/Confirm'
export default {
name: 'Default',
components: { Snackbar, Confirm },
created () {
this.$vuetify.theme.dark = false
}
}
</script>

View file

@ -1,10 +1,10 @@
<template lang='pug'> <template lang='pug'>
v-app v-app(app)
Snackbar Snackbar
Confirm Confirm
Nav Nav
v-main v-main(app)
v-fade-transition(hide-on-leave) v-fade-transition(hide-on-leave)
nuxt nuxt
@ -19,13 +19,18 @@ import Confirm from '../components/Confirm'
import { mapState } from 'vuex' import { mapState } from 'vuex'
export default { export default {
head () {
return {
htmlAttrs: {
lang: this.locale
}
}
},
name: 'Default', name: 'Default',
components: { Nav, Snackbar, Footer, Confirm }, components: { Nav, Snackbar, Footer, Confirm },
computed: mapState(['settings']), computed: mapState(['settings', 'locale']),
created () { created () {
this.$vuetify.theme.dark = this.settings['theme.is_dark'] this.$vuetify.theme.dark = this.settings['theme.is_dark']
this.$vuetify.theme.themes.dark.primary = this.settings['theme.primary']
this.$vuetify.theme.themes.light.primary = this.settings['theme.primary']
} }
} }
</script> </script>

View file

@ -2,7 +2,8 @@
v-container.p-4.text-center v-container.p-4.text-center
v-alert(v-if="error.statusCode === 404") ¯\_()_/¯ {{error.message}} v-alert(v-if="error.statusCode === 404") ¯\_()_/¯ {{error.message}}
v-alert(v-else type='error') <v-icon>mdi-warning</v-icon> An error occurred: {{error.message}} v-alert(v-else type='error') <v-icon>mdi-warning</v-icon> An error occurred: {{error.message}}
nuxt-link(to='/') Back to home nuxt-link(to='/')
v-btn Back to home
</template> </template>
<script> <script>

View file

@ -5,7 +5,7 @@
Nav Nav
v-main(app) v-main(app)
v-scroll-y-transition(hide-on-leave) v-fade-transition(hide-on-leave)
nuxt nuxt
Footer Footer

View file

@ -84,7 +84,9 @@
"import": "Importa", "import": "Importa",
"reset": "Reinicia", "reset": "Reinicia",
"theme": "Tema", "theme": "Tema",
"tags": "Etiquetes" "tags": "Etiquetes",
"label": "Etiqueta",
"max_events": "Nre. màx. d'activitats"
}, },
"login": { "login": {
"description": "Amb la sessió iniciada pots afegir activitats noves.", "description": "Amb la sessió iniciada pots afegir activitats noves.",
@ -122,7 +124,7 @@
"media_description": "Pots adjuntar un cartell (opcional)", "media_description": "Pots adjuntar un cartell (opcional)",
"added": "S'ha afegit l'activitat", "added": "S'ha afegit l'activitat",
"added_anon": "S'ha afegit l'activitat però encara ha de ser confirmada.", "added_anon": "S'ha afegit l'activitat però encara ha de ser confirmada.",
"where_description": "On es farà? Si no està posat, escriu-ho i <b>prem Enter</b>. ", "where_description": "On es farà? Si no apareix el lloc el pots crear.",
"confirmed": "S'ha confirmat l'activitat", "confirmed": "S'ha confirmat l'activitat",
"not_found": "No s'ha trobat l'activitat", "not_found": "No s'ha trobat l'activitat",
"remove_confirmation": "Segur que vols esborrar l'activitat?", "remove_confirmation": "Segur que vols esborrar l'activitat?",
@ -153,14 +155,18 @@
"remove_recurrent_confirmation": "Estàs segur/a d'esborrar aquesta activitat periòdica?\nNo s'esborraran les ocurrències antigues, només es deixaran de crear les futures.", "remove_recurrent_confirmation": "Estàs segur/a d'esborrar aquesta activitat periòdica?\nNo s'esborraran les ocurrències antigues, només es deixaran de crear les futures.",
"ics": "ICS", "ics": "ICS",
"import_ICS": "Importa des d'un ICS", "import_ICS": "Importa des d'un ICS",
"import_URL": "Importa des d'una URL" "import_URL": "Importa des d'una URL",
"saved": "S'ha desat l'activitat",
"import_description": "Pots importar activitats des d'altres instàncies o plataformes que facin servir formats estàndards (ics o h-event)",
"edit_recurrent": "Edita l'activitat periòdica:",
"updated": "S'ha actualitzat l'activitat"
}, },
"admin": { "admin": {
"place_description": "En el cas que un lloc és incorrecte o l'adreça ha de canviar, pots arreglar-ho.<br/>Tingues en compte que totes les activitats passades i futures associades amb aquest lloc també canviaran d'adreça.", "place_description": "En el cas que un lloc és incorrecte o l'adreça ha de canviar, pots arreglar-ho.<br/>Tingues en compte que totes les activitats passades i futures associades amb aquest lloc també canviaran d'adreça.",
"event_confirm_description": "Des d'aquí pots confirmar les activitats creades anònimament", "event_confirm_description": "Des d'aquí pots confirmar les activitats creades anònimament",
"delete_user": "Esborra", "delete_user": "Esborra",
"remove_admin": "Esborra admin", "remove_admin": "Esborra admin",
"delete_user_confirm": "Segur que vols esborrar aquest compte?", "delete_user_confirm": "Segur que vols esborrar el compte {user}?",
"user_remove_ok": "S'ha esborrat el compte", "user_remove_ok": "S'ha esborrat el compte",
"user_create_ok": "S'ha creat el compte", "user_create_ok": "S'ha creat el compte",
"allow_registration_description": "Vols deixar el registre obert?", "allow_registration_description": "Vols deixar el registre obert?",
@ -189,7 +195,7 @@
"resources": "Recursos", "resources": "Recursos",
"user_blocked": "L'usuari/a {user} ja no podrà afegir recursos", "user_blocked": "L'usuari/a {user} ja no podrà afegir recursos",
"favicon": "Logo", "favicon": "Logo",
"user_block_confirm": "Segur/a que vols bloquejar l'usuària?", "user_block_confirm": "Segur que vols bloquejar a {user}?",
"delete_announcement_confirm": "Segur/a que vols esborrar l'anunci?", "delete_announcement_confirm": "Segur/a que vols esborrar l'anunci?",
"announcement_remove_ok": "S'ha esborrat l'anunci", "announcement_remove_ok": "S'ha esborrat l'anunci",
"announcement_description": "En aquesta secció pots afegir anuncis que romandran a la pàgina principal", "announcement_description": "En aquesta secció pots afegir anuncis que romandran a la pàgina principal",
@ -210,7 +216,16 @@
"delete_footer_link_confirm": "Segur que vols esborrar aquest enllaç?", "delete_footer_link_confirm": "Segur que vols esborrar aquest enllaç?",
"footer_links": "Enllaços del peu", "footer_links": "Enllaços del peu",
"add_link": "Afegeix un enllaç", "add_link": "Afegeix un enllaç",
"is_dark": "Tema fosc" "is_dark": "Tema fosc",
"disable_user_confirm": "Segur que vols deshabilitar a {user}?",
"add_instance": "Afegeix una instància",
"instance_block_confirm": "Segur que vols bloquejar la instància {instance}?",
"show_smtp_setup": "Configuració de correu",
"smtp_hostname": "Amfitrió SMTP (hostname)",
"smtp_description": "<ul><li>L'admin hauria de rebre un correu cada cop que es pengi alguna una activitat anònima (si estan activades).</li><li>L'admin hauria de rebre un correu per cada soŀlicitud de registre (si estan actives).</li><li>La usuària hauria de rebre un correu després de soŀlicitar registrar-se.</li><li>La usuària hauria de rebre un correu quan se li hagi confirmat el registre.</li><li>La usuària hauria de rebre un correu si l'admin la registra directament.</li><li>La usuària hauria de rebre un correu de restabliment de contrasenya si ho demana</li></ul>",
"smtp_test_success": "S'ha enviat un correu de prova a {admin_email}, comprova que hagi arribat bé",
"smtp_test_button": "Envia un correu de prova",
"admin_email": "Correu d'admin"
}, },
"auth": { "auth": {
"not_confirmed": "Encara no s'ha confirmat…", "not_confirmed": "Encara no s'ha confirmat…",
@ -252,5 +267,11 @@
"validators": { "validators": {
"email": "Escriu una adreça de correu vàlida", "email": "Escriu una adreça de correu vàlida",
"required": "Cal omplir el camp {fieldName} és" "required": "Cal omplir el camp {fieldName} és"
},
"setup": {
"completed": "S'ha completat la configuració inicial",
"check_db": "Comprova la BD",
"completed_description": "<p>Ara ja pots entrar amb aquesta usuària:<br/><br/>Nom: <b>{email}</b><br/>Contrasenya: <b>{password}<b/></p>",
"start": "Comença"
} }
} }

View file

@ -1,7 +1,7 @@
{ {
"register": { "register": {
"subject": "Hem rebut una soŀlicitud de registre", "subject": "Hem rebut una soŀlicitud de registre",
"content": "Hem rebut una soŀlicitud de registre. Hi respondrem tan aviat com ens sigui possible.\nSalut" "content": "Hem rebut una soŀlicitud de registre. Hi respondrem tan aviat com ens sigui possible."
}, },
"confirm": { "confirm": {
"subject": "Ja pots publicar activitats", "subject": "Ja pots publicar activitats",
@ -18,5 +18,12 @@
"admin_register": { "admin_register": {
"subject": "Registre nou", "subject": "Registre nou",
"content": "{{user.email}} ha soŀlicitat regsitrar-se a {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Respon a la soŀlicitud <a href='{{config.baseurl}}/admin'>aquí</a>." "content": "{{user.email}} ha soŀlicitat regsitrar-se a {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Respon a la soŀlicitud <a href='{{config.baseurl}}/admin'>aquí</a>."
},
"event_confirm": {
"content": "Pots acceptar aquesta activitat a <a href='{{url}}'>la pàgina de confirmació</a>"
},
"test": {
"subject": "La configuració SMTP funciona",
"content": "Aquest és un correu de prova, si llegeixes això és que la configuració funciona."
} }
} }

View file

@ -18,5 +18,12 @@
"admin_register": { "admin_register": {
"subject": "New registration", "subject": "New registration",
"content": "{{user.email}} has requested registration on {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Confirm it <a href='{{config.baseurl}}/admin'>here</a>." "content": "{{user.email}} has requested registration on {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Confirm it <a href='{{config.baseurl}}/admin'>here</a>."
},
"event_confirm": {
"content": "You can confirm this event at <a href='{{url}}'>this page</a>"
},
"test": {
"subject": "Your SMTP configuration is working",
"content": "This is a test email, if you are reading this your configuration is working."
} }
} }

View file

@ -1,7 +1,7 @@
{ {
"register": { "register": {
"subject": "Solicitud de registro recibida", "subject": "Solicitud de registro recibida",
"content": "Recibimos la solicitud de registro. Lo confirmaremos tan pronto como podamos.\n Adios" "content": "Recibimos la solicitud de registro. Lo confirmaremos tan pronto como podamos."
}, },
"confirm": { "confirm": {
"subject": "Puedes empezar a publicar eventos", "subject": "Puedes empezar a publicar eventos",
@ -21,5 +21,8 @@
"admin_register": { "admin_register": {
"subject": "Nuevo registro", "subject": "Nuevo registro",
"content": "{{user.email}} ha pedido registrarse en {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Confírmalo <a href='{{config.baseurl}}/admin'>aquí</a>." "content": "{{user.email}} ha pedido registrarse en {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Confírmalo <a href='{{config.baseurl}}/admin'>aquí</a>."
},
"event_confirm": {
"content": "Puede confirmar este evento <a href='{{url}}'>aquí</a>"
} }
} }

View file

@ -18,5 +18,12 @@
"register": { "register": {
"content": "Nous avons reçu la demande d'inscription. Nous la confirmerons au plus vite.", "content": "Nous avons reçu la demande d'inscription. Nous la confirmerons au plus vite.",
"subject": "Demande d'inscription reçue" "subject": "Demande d'inscription reçue"
},
"event_confirm": {
"content": "Vous pouvez confirmer cet événement sur <a href='{{url}}'>cette page</a>"
},
"test": {
"subject": "Votre configuration SMTP est fonctionnelle",
"content": "Ceci est un e-mail de test, si vous pouvez lire ceci c'est que votre configuration est fonctionnelle."
} }
} }

View file

@ -18,5 +18,8 @@
"admin_register": { "admin_register": {
"subject": "Nuova registrazione", "subject": "Nuova registrazione",
"content": "{{user.email}} si è registratǝ a {{config.title}} scrivendo:<br/><pre>{{user.description}}</pre><br/> Puoi confermarlo <a href='{{config.baseurl}}/admin'>qui</a>." "content": "{{user.email}} si è registratǝ a {{config.title}} scrivendo:<br/><pre>{{user.description}}</pre><br/> Puoi confermarlo <a href='{{config.baseurl}}/admin'>qui</a>."
},
"event_confirm": {
"content": "Puoi confermare questo evento premendo il tasto conferma in <a href='{{url}}'>questa pagina</a>"
} }
} }

View file

@ -45,8 +45,8 @@
"new_user": "New user", "new_user": "New user",
"ok": "Ok", "ok": "Ok",
"cancel": "Cancel", "cancel": "Cancel",
"enable": "Turn on", "enable": "Enable",
"disable": "Turn off", "disable": "Disable",
"me": "You", "me": "You",
"password_updated": "Password changed.", "password_updated": "Password changed.",
"resources": "Resources", "resources": "Resources",
@ -123,6 +123,7 @@
"tag_description": "Tag", "tag_description": "Tag",
"media_description": "You can add a flyer (optional)", "media_description": "You can add a flyer (optional)",
"added": "Event added", "added": "Event added",
"saved": "Event saved",
"added_anon": "Event added, but has yet to be confirmed.", "added_anon": "Event added, but has yet to be confirmed.",
"updated": "Event updated", "updated": "Event updated",
"where_description": "Where's the event? If not present you can create it.", "where_description": "Where's the event? If not present you can create it.",
@ -165,7 +166,8 @@
"event_confirm_description": "You can confirm events entered by anonymous users here", "event_confirm_description": "You can confirm events entered by anonymous users here",
"delete_user": "Remove", "delete_user": "Remove",
"remove_admin": "Remove admin", "remove_admin": "Remove admin",
"delete_user_confirm": "Are you sure you want to remove this user?", "disable_user_confirm": "Are you sure you want to disable {user}?",
"delete_user_confirm": "Are you sure you want to remove {user}?",
"user_remove_ok": "User removed", "user_remove_ok": "User removed",
"user_create_ok": "User created", "user_create_ok": "User created",
"allow_registration_description": "Allow open registrations?", "allow_registration_description": "Allow open registrations?",
@ -195,7 +197,8 @@
"resources": "Resources", "resources": "Resources",
"user_blocked": "User {user} blocked", "user_blocked": "User {user} blocked",
"favicon": "Logo", "favicon": "Logo",
"user_block_confirm": "Are you sure you want block this user?", "user_block_confirm": "Are you sure you want to block user {user}?",
"instance_block_confirm": "Are you sure you want block instance {instance}?",
"delete_announcement_confirm": "Are you sure you want to remove the announcement?", "delete_announcement_confirm": "Are you sure you want to remove the announcement?",
"announcement_remove_ok": "Announce removed", "announcement_remove_ok": "Announce removed",
"announcement_description": "In this section you can insert announcements to remain on the homepage", "announcement_description": "In this section you can insert announcements to remain on the homepage",
@ -216,7 +219,14 @@
"footer_links": "Footer links", "footer_links": "Footer links",
"delete_footer_link_confirm": "Sure to remove this link?", "delete_footer_link_confirm": "Sure to remove this link?",
"edit_place": "Edit place", "edit_place": "Edit place",
"new_announcement": "New announcement" "new_announcement": "New announcement",
"show_smtp_setup": "Email settings",
"smtp_hostname": "SMTP Hostname",
"smtp_description": "<ul><li>Admin should receive an email when anon event is added (if enabled).</li><li>Admin should receive email of registration request (if enabled).</li><li>User should receive an email of registration request.</li><li>User should receive email of confirmed registration.</li><li>User should receive a confirmation email when subscribed directly by admin.</li><li>Users should receive email to restore password when they forgot it</li></ul>",
"smtp_test_success": "A test email is sent to {admin_email}, please check your inbox",
"smtp_test_button": "Send a test email",
"admin_email": "Admin e-mail"
}, },
"auth": { "auth": {
"not_confirmed": "Not confirmed yet…", "not_confirmed": "Not confirmed yet…",
@ -258,5 +268,10 @@
"scopes": { "scopes": {
"event:write": "Add and edit your events" "event:write": "Add and edit your events"
} }
},
"setup": {
"completed": "Setup completed",
"completed_description": "<p>You can now login with the following user:<br/><br/>User: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
"start": "Start"
} }
} }

View file

@ -46,7 +46,7 @@
"ok": "Ok", "ok": "Ok",
"cancel": "Cancelar", "cancel": "Cancelar",
"enable": "Habilitar", "enable": "Habilitar",
"disable": "Deshabilita", "disable": "Deshabilitar",
"me": "Tú", "me": "Tú",
"password_updated": "Contraseña actualizada.", "password_updated": "Contraseña actualizada.",
"comments": "ningún comentario|un comentario|{n} comentarios", "comments": "ningún comentario|un comentario|{n} comentarios",
@ -85,7 +85,9 @@
"tags": "Tags", "tags": "Tags",
"import": "Importar", "import": "Importar",
"reset": "Reset", "reset": "Reset",
"theme": "Tema" "theme": "Tema",
"label": "Etiqueta",
"max_events": "Número de eventos máximo"
}, },
"login": { "login": {
"description": "Entrando podrás publicar nuevos eventos.", "description": "Entrando podrás publicar nuevos eventos.",
@ -103,7 +105,7 @@
"intro": "A diferencia de las plataformas del capitalismo, que hacen todo lo posible para mantener datos y usuarios dentro de ellas, creemos las informaciones, así como las personas, deben ser libres. Para ello, puedes mantenerte enterado sobre los eventos que te interesan como mejor te parezca, sin necesariamente tener que pasar por este sitio.", "intro": "A diferencia de las plataformas del capitalismo, que hacen todo lo posible para mantener datos y usuarios dentro de ellas, creemos las informaciones, así como las personas, deben ser libres. Para ello, puedes mantenerte enterado sobre los eventos que te interesan como mejor te parezca, sin necesariamente tener que pasar por este sitio.",
"email_description": "Puedes recibir por mail los eventos que te interesan.", "email_description": "Puedes recibir por mail los eventos que te interesan.",
"insert_your_address": "Casilla de correo", "insert_your_address": "Casilla de correo",
"feed_description": "Para seguir las actualizaciones desde un ordenador o teléfono inteligente sin la necesidad de abrir periódicamente el sitio, el método recomendado es usar los feeds RSS.</p>\n\n <p>Con rss feeds, utilizás una aplicación especial para recibir actualizaciones de los sitios que más te interesan, como por ejemplo este. Es una buena manera de seguir muchos sitios muy rápidamente, sin la necesidad de crear una cuenta u otras complicaciones.</p>\n \n <li>Si tienes Android, te sugerimos <a href=\"https://f-droid.org/es/packages/com.nononsenseapps.feeder/\">Feeder</a> o Feeder</li>\n <li>Para iPhone/iPad puedes usar <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n <li>En el caso de un ordenador aconsejamos Feedbro, se instala como plugin <a href=\"https://addons.mozilla.org/es-ES/firefox/addon/feedbroreader/\">de Firefox </a>o <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">de Chrome</a> y funciona con todos los principales sistemas.</li>\n <br/>\n Agregando este link a tu lector de feed, estarás siempre actualizado/a.", "feed_description": "Para seguir las actualizaciones desde un ordenador o teléfono inteligente sin la necesidad de abrir periódicamente el sitio, el método recomendado es usar los feeds RSS.</p>\n\n <p>Con rss feeds, utilizas una aplicación especial para recibir actualizaciones de los sitios que más te interesan, como por ejemplo éste. Es una buena manera de seguir muchos sitios muy rápidamente, sin la necesidad de crear una cuenta u otras complicaciones.</p>\n \n <li>Si tienes Android, te sugerimos <a href=\"https://f-droid.org/es/packages/com.nononsenseapps.feeder/\">Feeder</a> o Feeder</li>\n <li>Para iPhone/iPad puedes usar <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n <li>En el caso de un ordenador aconsejamos Feedbro, se instala como plugin <a href=\"https://addons.mozilla.org/es-ES/firefox/addon/feedbroreader/\">de Firefox </a>o <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">de Chrome</a> y funciona con todos los principales sistemas.</li>\n <br/>\n Agregando este link a tu lector de feed, estarás siempre actualizado/a.",
"ical_description": "Las computadoras y los teléfonos inteligentes suelen estar equipados con una aplicación para administrar un calendario. Estos programas generalmente se pueden usar para importar un calendario remoto.", "ical_description": "Las computadoras y los teléfonos inteligentes suelen estar equipados con una aplicación para administrar un calendario. Estos programas generalmente se pueden usar para importar un calendario remoto.",
"list_description": "Si tienes un sitio web y quieres mostrar una lista de eventos, puedes usar el siguiente código" "list_description": "Si tienes un sitio web y quieres mostrar una lista de eventos, puedes usar el siguiente código"
}, },
@ -120,7 +122,7 @@
"what_description": "Nombre evento", "what_description": "Nombre evento",
"description_description": "Descripción, puedes copiar y pegar", "description_description": "Descripción, puedes copiar y pegar",
"tag_description": "Tag...", "tag_description": "Tag...",
"media_description": "Puedes agregar un panfleto (opcionál)", "media_description": "Puedes agregar una imagen (opcional)",
"added": "Evento agregado", "added": "Evento agregado",
"added_anon": "Evento agregado, será confirmado cuanto antes.", "added_anon": "Evento agregado, será confirmado cuanto antes.",
"where_description": "¿Dónde es? Si el lugar no está, escribilo.", "where_description": "¿Dónde es? Si el lugar no está, escribilo.",
@ -128,8 +130,8 @@
"not_found": "Evento no encontrado", "not_found": "Evento no encontrado",
"remove_confirmation": "¿Estás seguro/a de querér eliminar este evento?", "remove_confirmation": "¿Estás seguro/a de querér eliminar este evento?",
"recurrent": "Recurrente", "recurrent": "Recurrente",
"recurrent_description": "Elegí la frecuencia y selecciona los días.", "recurrent_description": "Elegí la frecuencia y selecciona los días",
"multidate_description": "¿Un festival o más de un día? Elegí cuándo comienza y cuándo termina.", "multidate_description": "¿Un festival o más de un día? Elegí cuándo comienza y cuándo termina",
"multidate": "Más días", "multidate": "Más días",
"normal": "Normal", "normal": "Normal",
"normal_description": "Selecciona el día.", "normal_description": "Selecciona el día.",
@ -154,14 +156,18 @@
"ics": "ICS", "ics": "ICS",
"import_ICS": "Importar desde ICS", "import_ICS": "Importar desde ICS",
"import_URL": "Importar desde la URL", "import_URL": "Importar desde la URL",
"only_future": "solo eventos venideros" "only_future": "solo eventos venideros",
"import_description": "Puedes importar eventos de otras plataformas y otras instancias mediante formatos estandars (ics y h-event)",
"edit_recurrent": "Editar evento recurrente:",
"updated": "Evento actualizado",
"saved": "Evento guardado"
}, },
"admin": { "admin": {
"place_description": "En el caso de que un lugar sea incorrecto o cambie de dirección, puedes cambiarlo. <br/> En este caso hay que tener en cuenta que todos los eventos asociados con ese lugar cambiarán de dirección (¡incluso los pasados!)", "place_description": "En el caso de que un lugar sea incorrecto o cambie de dirección, puedes cambiarlo. <br/> Todos los eventos presentes y pasados asociados con este lugar cambiarán de dirección.",
"event_confirm_description": "Puedes confirmar aquí los eventos agregados por usuarios anónimos", "event_confirm_description": "Puedes confirmar aquí los eventos agregados por usuarios anónimos",
"delete_user": "Elimina", "delete_user": "Elimina",
"remove_admin": "Borra admin", "remove_admin": "Borra admin",
"delete_user_confirm": "¿Estás seguro/a de borrar este usuario?", "delete_user_confirm": "¿Estás seguro/a de borrar a {user}?",
"user_remove_ok": "Usuario eliminado", "user_remove_ok": "Usuario eliminado",
"user_create_ok": "Usuario creado", "user_create_ok": "Usuario creado",
"allow_registration_description": "¿Querés habilitar el registro?", "allow_registration_description": "¿Querés habilitar el registro?",
@ -170,7 +176,7 @@
"allow_recurrent_event": "Habilitar eventos fijos", "allow_recurrent_event": "Habilitar eventos fijos",
"recurrent_event_visible": "Eventos fijos visibles por defecto", "recurrent_event_visible": "Eventos fijos visibles por defecto",
"federation": "Federación / ActivityPub", "federation": "Federación / ActivityPub",
"enable_federation": "Habilitar la federación!", "enable_federation": "Habilitar la federación",
"enable_federation_help": "Será posible seguir esta instancia desde el fediverso", "enable_federation_help": "Será posible seguir esta instancia desde el fediverso",
"select_instance_timezone": "Uso horario", "select_instance_timezone": "Uso horario",
"enable_resources": "Habilitar recursos", "enable_resources": "Habilitar recursos",
@ -179,7 +185,7 @@
"hide_boost_bookmark_help": "Oculta los pequeños iconos que muestran el número de impulsos y marcadores que vienen del fediverso", "hide_boost_bookmark_help": "Oculta los pequeños iconos que muestran el número de impulsos y marcadores que vienen del fediverso",
"block": "Bloquear", "block": "Bloquear",
"unblock": "Desbloquear", "unblock": "Desbloquear",
"user_add_help": "Enviaremos un correo electrónico al nuevo usuario con instrucciones para confirmar la suscripción y elegir una contraseña.", "user_add_help": "Enviaremos un correo electrónico al nuevo usuario con instrucciones para confirmar la suscripción y elegir una contraseña",
"instance_name": "Nombre de la instancia", "instance_name": "Nombre de la instancia",
"show_resource": "Mostrar recurso", "show_resource": "Mostrar recurso",
"hide_resource": "Ocultar recurso", "hide_resource": "Ocultar recurso",
@ -191,7 +197,7 @@
"resources": "Recursos", "resources": "Recursos",
"user_blocked": "El usuario {usuario} ya no podrá añadir recursos", "user_blocked": "El usuario {usuario} ya no podrá añadir recursos",
"favicon": "Logo", "favicon": "Logo",
"user_block_confirm": "¿Estás seguro de que quieres bloquear al usuario?", "user_block_confirm": "¿Estás seguro de que quieres bloquear a {user}?",
"delete_announcement_confirm": "¿Estás seguro de que quieres borrar el anuncio?", "delete_announcement_confirm": "¿Estás seguro de que quieres borrar el anuncio?",
"announcement_remove_ok": "Anuncio borrado", "announcement_remove_ok": "Anuncio borrado",
"announcement_description": "En esta sección se pueden insertar anuncios que permanecerán en la página de inicio", "announcement_description": "En esta sección se pueden insertar anuncios que permanecerán en la página de inicio",
@ -212,7 +218,10 @@
"delete_footer_link_confirm": "Seguro que quieres quitar este enlace?", "delete_footer_link_confirm": "Seguro que quieres quitar este enlace?",
"footer_links": "Enlaces a pie de página", "footer_links": "Enlaces a pie de página",
"add_link": "Añadir enlace", "add_link": "Añadir enlace",
"is_dark": "Tema oscuro" "is_dark": "Tema oscuro",
"instance_block_confirm": "¿Estás seguro/a que quieres bloquear la instancia {instance}?",
"add_instance": "Añadir instancia",
"disable_user_confirm": "Estas seguro de que quieres deshabilitar a {user}?"
}, },
"auth": { "auth": {
"not_confirmed": "Todavía no hemos confirmado este email…", "not_confirmed": "Todavía no hemos confirmado este email…",
@ -227,7 +236,7 @@
"update_confirm": "¿Estás seguro de que quieres guardar los cambios?" "update_confirm": "¿Estás seguro de que quieres guardar los cambios?"
}, },
"error": { "error": {
"nick_taken": "Este nickname ya está registrado", "nick_taken": "Este apodo ya está registrado.",
"email_taken": "Este correo electrónico ya está registrado." "email_taken": "Este correo electrónico ya está registrado."
}, },
"ordinal": { "ordinal": {
@ -238,11 +247,11 @@
"5": "quinto", "5": "quinto",
"-1": "último" "-1": "último"
}, },
"about": "\n <p>\n Gancio es un proyecto del <a href='https://autistici.org/underscore'>underscore hacklab</a> y es uno de los\n servicios de <a href='https://cisti.org'>cisti.org</a>.</p>\n\n <h5>¿Que es gancio?</h5>\n <p>Gancio (se pronuncia \"gancho\") es una herramienta para compartir eventos orientado a las comunidades radicales.\n Dentro del gancio pueden encontrar y agregar eventos.\n Gancio, como todo <a href='https://cisti.org'> cisti.org </a> es una herramienta\n antisexista, antirracista, antifascista y anticapitalista, así que piensen en eso cuando\n van a publicar un evento. </p>\n\n <h5>Ok, pero ¿que quiere decir gancio?</h5>\n <p>\n Literalmente sería \"enganche\", pero en realidad viene de una forma de decir que se usa en en Turín (Italia). Ahí si alguien dice: \"ehi, ci diamo un gancio alle 8?\" (\"ehi, ¿nos damos un enganche a las 8?\") quiere decir \"ehí, ¿nos vemos a las 8?\". \"Darsi un gancio\" es juntarse a una hora X en un lugar Y.</p>\n <code>\n <ul>\n <li> ¿A qué hora es el <i>gancio</i> para ir a la marcha?</li>\n <li> No sé, de todos modos no puedo ir, ya tengo un <i>gancio</i> para ir a una reunión.</li>\n </ul>\n </code>\n\n <h5> Contactos</h5>\n <p>\n ¿Escribiste una nueva interfaz para gancio? ¿Quieres abrir un gancio en tu ciudad?\n ¿Hay algo que te gustaría mejorar? Para contribuir el código fuente es libre y disponible \n <a href='https://git.lattuga.net/cisti/gancio'>aquí</a>. Ayuda y sugerencias son siempre bienvenidos, puedes comunicarte con nosotros \n enviando un mail a underscore arroba autistici.org</p>", "about": "\n <p><a href='https://gancio.org'>Gancio</a> es una agenda compartida para comunidades locales.</p>\n ",
"confirm": { "confirm": {
"title": "Confirmación de usuario", "title": "Confirmación de usuario",
"not_valid": "Mmmmm algo salió mal.", "not_valid": "Mmmmm algo salió mal.",
"valid": "Su cuenta ha sido confirmada, ahora puede <a href=\"/login\">ingresar</a>." "valid": "Su cuenta ha sido confirmada, ahora puede <a href=\"/login\">ingresar</a>"
}, },
"oauth": { "oauth": {
"authorization_request": "La aplicación externa <code>{app}</code> requiere permiso para realizar las siguientes tareas en <code>{instance_name}</code>:", "authorization_request": "La aplicación externa <code>{app}</code> requiere permiso para realizar las siguientes tareas en <code>{instance_name}</code>:",

View file

@ -48,7 +48,7 @@
"enable": "Gaitu", "enable": "Gaitu",
"disable": "Desgaitu", "disable": "Desgaitu",
"me": "Zu", "me": "Zu",
"password_updated": "Pasahitza eguneratuta!", "password_updated": "Pasahitza eguneratuta.",
"activate_user": "Egiaztatuta", "activate_user": "Egiaztatuta",
"displayname": "Erakutsitako izena", "displayname": "Erakutsitako izena",
"federation": "Federazioa", "federation": "Federazioa",
@ -80,37 +80,43 @@
"delete": "Ezabatu", "delete": "Ezabatu",
"announcements": "Iragarkiak", "announcements": "Iragarkiak",
"url": "URL esteka", "url": "URL esteka",
"place": "Lekua" "place": "Lekua",
"label": "Etiketa",
"max_events": "Max zenbakidun gertaerak",
"import": "Inportatu",
"reset": "Zeroan jarri",
"theme": "Gai",
"tags": "Tags"
}, },
"login": { "login": {
"description": "Saioa hasiz gero, ekitaldi berriak sortu ahal izango dituzu", "description": "Saioa hasiz gero, ekitaldi berriak sortu ahal izango dituzu.",
"check_email": "Begiratu zure postontzi elektronikoan, baita mezu baztergarrietan", "check_email": "Begiratu zure postontzi elektronikoan, baita mezu baztergarrietan.",
"not_registered": "Ez duzu izena eman?", "not_registered": "Ez duzu izena eman?",
"forgot_password": "Pasahitza ahaztu duzu?", "forgot_password": "Pasahitza ahaztu duzu?",
"error": "Ezin da saioa hasi, egiaztatu zure datuok.", "error": "Ezin da saioa hasi, egiaztatu zure datuok.",
"insert_email": "Sartu zure helbide elektronikoa", "insert_email": "Sartu zure helbide elektronikoa",
"ok": "Saioa hasi duzu!" "ok": "Saioa hasi duzu"
}, },
"recover": { "recover": {
"not_valid_code": "Mmmmm zerbaitek huts egin du..." "not_valid_code": "Mmmmm zerbaitek huts egin du..."
}, },
"export": { "export": {
"intro": "Kapitalismoaren plataformek edozer egingo dute erabiltzaileak eta haien datuak gordetzeko. Guk aldiz, informazioak, pertsonen antzera askeak izan behar dutela sinesten dugu. Horretarako gogoko dituzun ekitaldietaz info eguneratuak jaso ditzakezu webgune honetatik pasatzeko beharrik gabe.", "intro": "Kapitalismoaren plataformek edozer egingo dute erabiltzaileak eta haien datuak gordetzeko. Guk aldiz, informazioak, pertsonen antzera askeak izan behar dutela sinesten dugu. Horretarako gogoko dituzun ekitaldietaz info eguneratuak jaso ditzakezu webgune honetatik pasatzeko beharrik gabe.",
"email_description": "Interesatzen zaizkizun ekitaldiak jaso ditzakezu posta elektronikoan", "email_description": "Interesatzen zaizkizun ekitaldiak jaso ditzakezu posta elektronikoan.",
"insert_your_address": "Sartu zure helbide elektronikoa", "insert_your_address": "Sartu zure helbide elektronikoa",
"feed_description": "Eguneraketak sakelekoan edo ordenagailuan jaso nahi badituzu webgune hau bisitatu gabe, RSS jarioa erabiltzea gomendatzen dizugu.</p>\n<p>RSS jarioarentzat aplikazio berezi bat erabiliko duzu gogoko dituzun weguneetatik berriak jasotzeko. Oso modu egokia da gune askotako berriak erraz eta azkar jasotzeko eta ez da konturik sortu behar! </p>\n\n<li>Android baldin badaukazu <a href=\"https://play.google.com/store/apps/details?id=net.frju.flym\">Flym</a> edo Feeder gomendatzen dizugu</li>\n<li>iPhone/iPad-erako eskuragarri daukazu <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n<li>Ordenagailuaren kasuan Feedbro iradokitzen dugu, <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\">Firefoxeko</a> edo <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">Chromeko</a> gehigarri gisa instalatzen da eta sistema gehienetan dabil.</li>\n<br/>\nHonako esteka jario irakurgailuan sartuta, eguneraketa guztiak jasoko dituzu.", "feed_description": "Eguneraketak sakelekoan edo ordenagailuan jaso nahi badituzu webgune hau bisitatu gabe, RSS jarioa erabiltzea gomendatzen dizugu.</p>\n\n<p>RSS jarioarentzat aplikazio berezi bat erabiliko duzu gogoko dituzun weguneetatik berriak jasotzeko. Oso modu egokia da gune askotako berriak erraz eta azkar jasotzeko eta ez da konturik sortu behar! </p>\n\n<li>Android baldin badaukazu <a href=\"https://play.google.com/store/apps/details?id=net.frju.flym\">Flym</a> edo Feeder gomendatzen dizugu</li>\n<li>iPhone/iPad-erako eskuragarri daukazu <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n<li>Ordenagailuaren kasuan Feedbro iradokitzen dugu, <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\">Firefoxeko</a> edo <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">Chromeko</a> gehigarri gisa instalatzen da eta sistema gehienetan dabil.</li>\n<br/>\nHonako esteka jario irakurgailuan sartuta, eguneraketa guztiak jasoko dituzu.",
"ical_description": "Normalean ordenagailuak eta smartphoneak egutegiak inportatu eta kudeatzeko aplikazioekin etorri ohi dira", "ical_description": "Normalean ordenagailuak eta smartphoneak egutegiak inportatu eta kudeatzeko aplikazioekin etorri ohi dira.",
"list_description": "Webgune bat baduzu eta ekitaldien zerrenda erakutsi nahi baduzu, ondorengo kodea erabili dezakezu" "list_description": "Webgune bat baduzu eta ekitaldien zerrenda erakutsi nahi baduzu, ondorengo kodea erabili dezakezu"
}, },
"register": { "register": {
"description": "Herri mugimenduek autoantolaketaren bidean diru-iturrien beharrak dauzkatela badakigu.<br/>Honako hauxe oparitxoa da, hortaz erabili ezazue ekitaldi ez-komertzialak iragartzeko, eta esan gabe doa, ekitaldi antifaxistak, antisexistak eta antiarriztetarako :) . \n<br/>Argitaratzen hasi baino lehen<strong> zure kontu berriak onarpena jaso beharko du </strong>beraz, <strong>webgune honen atzean hezur-haragizko pertsonak gaudela jakinda </strong>, (momenutz euskal 'AI'-rik ez daukagu baina adi, agertuko direla) idatzi iezaguzu lerro batzuk argitaratu nahi dituzun ekitaldiei buruz", "description": "Herri mugimenduek autoantolaketaren bidean diru-iturrien beharrak dauzkatela badakigu.<br/>Honako hauxe oparitxoa da, hortaz erabili ezazue ekitaldi ez-komertzialak iragartzeko, eta esan gabe doa, ekitaldi antifaxistak, antisexistak eta antiarriztetarako :) .\n<br/>Argitaratzen hasi baino lehen<strong> zure kontu berriak onarpena jaso beharko du </strong>beraz, <strong>webgune honen atzean hezur-haragizko pertsonak gaudela jakinda </strong>, (momenutz euskal 'AI'-rik ez daukagu baina adi, agertuko direla) idatzi iezaguzu lerro batzuk argitaratu nahi dituzun ekitaldiei buruz.",
"error": "Hutsa: ", "error": "Hutsa: ",
"complete": "Izen-ematea baieztatu behar dute.", "complete": "Izen-ematea baieztatu behar dute.",
"first_user": "Administratzailea sortu da" "first_user": "Administratzailea sortu da"
}, },
"event": { "event": {
"anon": "Ezezaguna", "anon": "Ezezaguna",
"anon_description": "Ekitaldia sortu dezakezu <a href='/login'>saioa hasi</a> edo <a href='/register'>izena eman</a> gabe,\nbaina kasu honetan norbaitek egiaztatu beharko du ekitaldia gune honetarako egokia dela eta itxaron beharko duzu. Gainera, behin egiaztatuta hura aldatzea ez da posiblea izango.<br/><br/>\n Dena den, ahalik eta azkarren erantzuten saiatuko gara.", "anon_description": "Ekitaldia sortu dezakezu <a href='/login'>saioa hasi</a> edo <a href='/register'>izena eman</a> gabe,\nbaina kasu honetan norbaitek egiaztatu beharko du ekitaldia gune honetarako egokia dela eta itxaron beharko duzu. Gainera, behin egiaztatuta hura aldatzea ez da posiblea izango.<br/><br/>\nDena den, ahalik eta azkarren erantzuten saiatuko gara. ",
"same_day": "egun berean", "same_day": "egun berean",
"what_description": "Ekitaldiaren izena", "what_description": "Ekitaldiaren izena",
"description_description": "Ekitaldiaren azalpena", "description_description": "Ekitaldiaren azalpena",
@ -118,16 +124,16 @@
"media_description": "Eskuorria edo irudia gehitu dezakezu (aukerakoa)", "media_description": "Eskuorria edo irudia gehitu dezakezu (aukerakoa)",
"added": "Ekitaldia sortu da", "added": "Ekitaldia sortu da",
"added_anon": "Ekitaldia sortu da, baina baieztatzear dago.", "added_anon": "Ekitaldia sortu da, baina baieztatzear dago.",
"where_description": "Non da ekitaldia? Lekua ez bada zerrendan agertzen idatzi ezazu eta <b>enter sakatu</b>. ", "where_description": "Non da ekitaldia? Lekua ez bada zerrendan agertzen idatzi ezazu eta <b>enter sakatu</b>.",
"confirmed": "Ekitaldia egiaztatu da", "confirmed": "Ekitaldia egiaztatu da",
"not_found": "Ezin da ekitaldia aurkitu", "not_found": "Ezin da ekitaldia aurkitu",
"remove_confirmation": "Ziur zaude ekitaldi hau ezabatu nahi duzula?", "remove_confirmation": "Ziur zaude ekitaldi hau ezabatu nahi duzula?",
"remove_recurrent_confirmation": "Ziur zaude ekitaldi errepikari hau ezabatu nahi duzula??\n\nIragan diren ekitaldiak mantenduko dira, baina ez da ekitaldi berririk sortuko.", "remove_recurrent_confirmation": "Ziur zaude ekitaldi errepikari hau ezabatu nahi duzula?\nIragan diren ekitaldiak mantenduko dira, baina ez da ekitaldi berririk sortuko.",
"recurrent": "Errepikaria", "recurrent": "Errepikaria",
"show_recurrent": "Ekitaldi errepikariak", "show_recurrent": "Ekitaldi errepikariak",
"show_past": "Erakutsi iraganeko ekitaldiak", "show_past": "Erakutsi iraganeko ekitaldiak",
"recurrent_description": "Aukera ezazu maiztasuna eta hautatu egunak", "recurrent_description": "Aukera ezazu maiztasuna eta hautatu egunak",
"multidate_description": "Egun bat baino gehiagoko jaialdia da? Aukeratu noiz hasten den eta noiz amaitzen den.", "multidate_description": "Egun bat baino gehiagoko jaialdia da? Aukeratu noiz hasten den eta noiz amaitzen den",
"multidate": "Egun gehiagotan", "multidate": "Egun gehiagotan",
"normal": "Egunekoa", "normal": "Egunekoa",
"normal_description": "Eguna aukeratu.", "normal_description": "Eguna aukeratu.",
@ -143,27 +149,36 @@
"due": "Amaiera ordua", "due": "Amaiera ordua",
"from": "Hasiera ordua", "from": "Hasiera ordua",
"image_too_big": "Irudia handiegia omen da (4mb gehienez)", "image_too_big": "Irudia handiegia omen da (4mb gehienez)",
"interact_with_me": "Elkar gaitezen fedibertsoan: ", "interact_with_me": "Elkar gaitezen fedibertsoan",
"follow_me_description": " {title}n argitaratutako ekitaldien berri izateko aukeren artean,\n fedibertsoko <u>{account}</u> kontuari jarraitzea daukazu. Horretarako Mastodon erabili dezakezu, eta bertatik baliabideak gehitu ekitaldi baten.<br/><br/>\n Mastodon eta Fedibertsoa zer diren ez badakizu <a href='https://es.wikipedia.org/wiki/Fediverso'>artikulu hau</a> irakurtzea iradokitzen dizugu.<br/><br/> Sartu zure instantzia behean (adibidez mastodon.eus edo mastodon.jalgi.eus)" "follow_me_description": "{title}n argitaratutako ekitaldien berri izateko aukeren artean,\n fedibertsoko <u>{account}</u> kontuari jarraitzea daukazu. Horretarako Mastodon erabili dezakezu, eta bertatik baliabideak gehitu ekitaldi baten.<br/><br/>\n Mastodon eta Fedibertsoa zer diren ez badakizu <a href='https://es.wikipedia.org/wiki/Fediverso'>artikulu hau</a> irakurtzea iradokitzen dizugu.<br/><br/> Sartu zure instantzia behean (adibidez mastodon.eus edo mastodon.jalgi.eus)",
"import_description": "Beste plataforma eta adibide batzuetako gertaerak formatu estandarren bidez inportatu ditzakezu (ics eta h-event)",
"ics": "ICS",
"import_ICS": "ICS-ko inportazioa",
"import_URL": "URL-ko inportazioa",
"interact_with_me_at": "Hitz egin nirekin fediversoan",
"only_future": "gertakizunak besterik ez",
"edit_recurrent": "Gertaera errepikakorra:",
"updated": "Gertaera eguneratua",
"saved": "Gertaera salbatua"
}, },
"admin": { "admin": {
"place_description": "Lekuaren zehaztapenak aldatu ditzakezu, bai gaizki idatzita dagoelako, bai helbidez aldatu delako.<br/> Ondorioz, leku horrekin lotutako ekitaldi guztiak helbidez aldatuko direla kontuan hartu behar da (baita iraganekoak ere!)", "place_description": "Lekuaren zehaztapenak aldatu ditzakezu, bai gaizki idatzita dagoelako, bai helbidez aldatu delako.<br/> Ondorioz, leku horrekin lotutako ekitaldi guztiak helbidez aldatuko direla kontuan hartu behar da (baita iraganekoak ere!)",
"event_confirm_description": "Erabiltzaile ezezagunek sortutako ekitaldiak hemen egiaztatu ditzakezu", "event_confirm_description": "Erabiltzaile ezezagunek sortutako ekitaldiak hemen egiaztatu ditzakezu",
"delete_user": "Erabiltzailea ezabatu", "delete_user": "Erabiltzailea ezabatu",
"remove_admin": "Administratzailea ezabatu", "remove_admin": "Administratzailea ezabatu",
"delete_user_confirm": "Ziur zaude erabiltzailea ezabatu nahi duzula?", "delete_user_confirm": "Ziur zaude {user} ezabatu nahi duzula?",
"user_remove_ok": "Erabiltzailea ezabatu da", "user_remove_ok": "Erabiltzailea ezabatu da",
"user_create_ok": "Erabiltzailea sortu da", "user_create_ok": "Erabiltzailea sortu da",
"allow_registration_description": "Izen-emateak ahalbidetu nahi dituzu?", "allow_registration_description": "Izen-emateak ahalbidetu nahi dituzu?",
"allow_anon_event": "Ezezagunek ekitaldiak sortzea ahalbidetu nahi duzu? (Beti ere baieztapenarekin) ", "allow_anon_event": "Ezezagunek ekitaldiak sortzea ahalbidetu nahi duzu? (Beti ere baieztapenarekin)",
"allow_recurrent_event": "Ekitaldi errepikariak ahalbidetu?", "allow_recurrent_event": "Ekitaldi errepikariak ahalbidetu",
"recurrent_event_visible": "Erakutsi ekitaldi errepikariak modu lehenetsian", "recurrent_event_visible": "Erakutsi ekitaldi errepikariak modu lehenetsian",
"federation": "Federazioa / ActivityPub", "federation": "Federazioa / ActivityPub",
"enable_federation": "Federatzea gaitu", "enable_federation": "Federatzea gaitu",
"enable_federation_help": "Instantzia hau fedibertsoan jarraitzea gaituko duzu?", "enable_federation_help": "Instantzia hau fedibertsoan jarraitzea gaituko duzu",
"select_instance_timezone": "Ordu-eremua", "select_instance_timezone": "Ordu-eremua",
"instance_timezone_description": "Gancio hiri baten moduko lekuen ekitaldiak biltzeko diseinatuta dago. Leku honen ordu-eremua hautatuz gero ekitaldi gutziek ordu-eremu horrekiko adieraziko dira..", "instance_timezone_description": "Gancio hiri baten moduko lekuen ekitaldiak biltzeko diseinatuta dago. Leku honen ordu-eremua hautatuz gero ekitaldi gutziek ordu-eremu horrekiko adieraziko dira..",
"enable_resources": "Baliabideak gaitu ", "enable_resources": "Baliabideak gaitu",
"enable_resources_help": "Fedibertsotik ekitaldietan baliabideak gehitzea ahalbidetzen du", "enable_resources_help": "Fedibertsotik ekitaldietan baliabideak gehitzea ahalbidetzen du",
"hide_boost_bookmark": "Bultzadak eta laster-markak ezkutatu", "hide_boost_bookmark": "Bultzadak eta laster-markak ezkutatu",
"hide_boost_bookmark_help": "Fedibertsotik datozen bultzaden eta laster-marken ikonotxoak ezkutatzen ditu", "hide_boost_bookmark_help": "Fedibertsotik datozen bultzaden eta laster-marken ikonotxoak ezkutatzen ditu",
@ -181,37 +196,46 @@
"filter_users": "Erabiltzaileak iragazi", "filter_users": "Erabiltzaileak iragazi",
"instance_name": "Instantziaren izena", "instance_name": "Instantziaren izena",
"favicon": "Iruditxoa", "favicon": "Iruditxoa",
"user_block_confirm": "Ziur zaude erabiltzailea blokeatu nahi duzula?", "user_block_confirm": "Ziur zaude {user} blokeatu nahi duzula?",
"delete_announcement_confirm": "Ziur zaude iragarkia ezabatu nahi duzula?", "delete_announcement_confirm": "Ziur zaude iragarkia ezabatu nahi duzula?",
"announcement_remove_ok": "Iragarkia ezabatu da", "announcement_remove_ok": "Iragarkia ezabatu da",
"announcement_description": "Atal honetan iragarkiak txertatu ditzakezu hasiera-orrian ager daitezen", "announcement_description": "Atal honetan iragarkiak txertatu ditzakezu hasiera-orrian ager daitezen",
"instance_locale": "Instantziaren hizkuntza lehenetsia", "instance_locale": "Instantziaren hizkuntza lehenetsia",
"instance_locale_description": "Orriak erakusteko erabilitako hizkuntza erabiltzaileak nahiago duen hizkuntza da. Hala ere, kasu batzuetan mezuak modu berean erakutsi behar ditugu guztiontzat (adibidez ActivityPub-etik argitaratzen dugunean edo posta elektroniko batzuk bidaltzerakoan). Kasu hauetan goian hautatutako hizkuntza erabiliko dugu.", "instance_locale_description": "Orriak erakusteko erabilitako hizkuntza erabiltzaileak nahiago duen hizkuntza da. Hala ere, kasu batzuetan mezuak modu berean erakutsi behar ditugu guztiontzat (adibidez ActivityPub-etik argitaratzen dugunean edo posta elektroniko batzuk bidaltzerakoan). Kasu hauetan goian hautatutako hizkuntza erabiliko dugu.",
"instance_place": "Instantziaren kokalekua ", "instance_place": "Instantziaren kokalekua",
"title_description": "Orriaren izenburuan, jario eta ics-en esportazioan eta mezu elektronikoen gaian erabiliko da ", "title_description": "Orriaren izenburuan, jario eta ics-en esportazioan eta mezu elektronikoen gaian erabiliko da.",
"description_description": "Orriburuan agertuko da, izenburuarekin batera", "description_description": "Orriburuan agertuko da, izenburuarekin batera",
"instance_name_help": "Instantziaren kontua ActivityPub-en ", "instance_name_help": "Instantziaren kontua ActivityPub-en",
"enable_trusted_instances": "Kideko instantziak gaitu", "enable_trusted_instances": "Kideko instantziak gaitu",
"trusted_instances_help": "Kideko instantzien zerrenda orri-buruan agertuko dira", "trusted_instances_help": "Kideko instantzien zerrenda orri-buruan agertuko dira",
"add_trusted_instance": "Gehitu kideko instantzia bat", "add_trusted_instance": "Gehitu kideko instantzia bat",
"instance_place_help": "Beste instantzien zerrendetan agertuko den izena ", "instance_place_help": "Beste instantzien zerrendetan agertuko den izena",
"delete_trusted_instance_confirm": "Ziur zaude kideko instantzia hau zerrendatik ezabatu nahi duzula?" "delete_trusted_instance_confirm": "Ziur zaude kideko instantzia hau zerrendatik ezabatu nahi duzula?",
"new_announcement": "Iragarpen berria",
"edit_place": "Leku ederrean",
"delete_footer_link_confirm": "Ziur lotura kenduko duzula?",
"footer_links": "Oinezkoen konexioak",
"add_link": "Gehitu lotura",
"is_dark": "Gai iluna",
"instance_block_confirm": "Ziur al zaude blokearen adibidea {instance} nahi duzula?",
"add_instance": "Gehitu adibidea",
"disable_user_confirm": "Ziur zaude {user} deskonektatu nahi duzula?"
}, },
"auth": { "auth": {
"not_confirmed": "Oraindik baieztatu gabe dago...", "not_confirmed": "Oraindik baieztatu gabe dago",
"fail": "Saioa hasteak huts egin du! Ziur zaude datuok ondo daudela?" "fail": "Saioa hasteak huts egin du! Ziur zaude datuok ondo daudela?"
}, },
"settings": { "settings": {
"update_confirm": "Aldaketak gorde nahi duzu?", "update_confirm": "Aldaketak gorde nahi duzu?",
"change_password": "Pasahitza aldatu", "change_password": "Pasahitza aldatu",
"password_updated": "Pasahitza eguneratu da", "password_updated": "Pasahitza eguneratu da.",
"danger_section": "Atal arriskutsua", "danger_section": "Atal arriskutsua",
"remove_account": "Ondorengo botoia zapalduz gero zure erabiltzailea ezabatuko da. Argitaratutako ekitaldiak ordea, ez dira ezabatuko", "remove_account": "Ondorengo botoia zapalduz gero zure erabiltzailea ezabatuko da. Argitaratutako ekitaldiak ordea, ez dira ezabatuko.",
"remove_account_confirm": "Zure kontua behin betiko ezabatzear zaude" "remove_account_confirm": "Zure kontua behin betiko ezabatzear zaude"
}, },
"error": { "error": {
"nick_taken": "Dagoeneko ezizen hau hartuta dago", "nick_taken": "Dagoeneko ezizen hau hartuta dago.",
"email_taken": "Dagoeneko posta elektroniko hau hartuta dago" "email_taken": "Dagoeneko posta elektroniko hau hartuta dago."
}, },
"confirm": { "confirm": {
"title": "Erabiltzaile-baieztapena", "title": "Erabiltzaile-baieztapena",
@ -226,12 +250,16 @@
"5": "bostgarrena", "5": "bostgarrena",
"-1": "azkena" "-1": "azkena"
}, },
"about": "<div><h1><strong><u>Descarga la agenda semanal en pdf lista para imprimir pinchando </u></strong><a href='https://lubakiagenda.net/agenda.pdf' rel='noopener noreferrer nofollow'><strong><u>aquí.</u></strong></a></h1><p></p><p><strong>¿Quiénes somos?</strong></p><p>Somos un grupo de personas que gestionamos la lubakiagenda digital y creamos y colgamos la agenda semanal en pdf y papel. No generamos contenido, solamente moderamos el contenido de la web y subimos las actividades de las que nos enteramos.</p><p><strong>¿Qué es LubakiAgenda?</strong></p><p>Es la agenda social alternativa de Bilboalde*. Tiene su versión digital, que cada colectivo o espacio autogestionado puede actualizar con su propia programación, y una versión imprimible que se cuelga cada miércoles en la web y en los lugares más frecuentados de Bilbo.</p><p><strong>¿Cuál es el objetivo de LubakiAgenda?</strong></p><p>Una parte de las actividades que se incluyen en la agenda son las organizadas por gaztetxes, ateneos, distribuidoras,… y por el amplio movimiento popular y juvenil de Bilbo y alrededores. Queremos que esta agenda sea el reflejo de lo que organiza este movimiento en su trabajo cotidiano, resaltando que no hacen falta ni instituciones ni subvenciones para mantener en marcha la cultura popular.</p><p>Nuestro objetivo es dar difusión al movimiento popular desde una perspectiva anticapitalista, antifascista, antirracista, feminista e inclusiva.</p><p>Por ello no se publicarán actividades que vayan en contra de nuestros principios ni por regla general, tampoco actividades comerciales o de agentes sociales que consideremos que ya tienen sus propios medios y fuerzas de difusión y organización (como instituciones, partidos políticos o sindicatos mayoritarios)</p><p><strong>¿Cómo puedo participar?</strong></p><p>Si formas parte de un colectivo social, puedes colgar directamente las actividades que realicéis en el apartado superior derecho de la web (+ Nuevo evento) y solicitar que generemos una usuaria para tu colectivo. De este modo vuestra programación quedará colgada automáticamente sin necesidad de moderación.</p><p>También nos puedes escribir a <a href='mailto:agenda@lubakiagenda.net' rel='noopener noreferrer nofollow'><u>agenda@lubakiagenda.net</u></a> y mandarnos la programación de tu colectivo o espacio y nosotras la subiremos.</p><p>Es muy importante que si quieres que tu programación aparezca en la versión imprimible, <strong>nos hagas llegar la información antes del miércoles al mediodía de cada semana</strong> (si son actividades periódicas, no hace falta que nos mandes mail todas las semanas)</p><p>Si no perteneces a ningún colectivo pero quieres colgar actividades, siempre puedes subirlas mediante el apartado superior derecho de la web (+ Nuevo evento), pero debes saber que el contenido subido será sujeto a moderación para evitar duplicados o actividades contrarias a nuestros principios.</p><h5>¿Zer da 'Gancio'?</h5><p>Gancio, <a href='https://autistici.org/underscore'>Underscore hacklabeko</a> proiektua da eta <a href='https://cisti.org'>cisti.org-eko</a> zerbitzuetariko bat.</p> <p>Gancio ( \"gantzio\" ahoskatzen da) ekitaldiak zabaltzeko tresna da eta komunitate erradikalei zuzenduta dago. Bertan ekitaldiak aurkitu eta sortu daitezke. Gainera, <a href='https://cisti.org'>Cisti.org</a> osoak bezala, Ganciok izaera antisexista, antiarrazista, antifaxista eta antikapitalista dauka, beraz, izan hori buruan ekitaldia argitaratzera zoazenean.</p><h5>Ados, baina ¿zer arraio esan nahi du 'gancio' hitzak?</h5><p>Literalki \"kakoa\" litzateke, baina egia esan Turinen (Italia) erabiltzen den esaeratik dator, hau da, norbaitek esaten badu: \n \"- ehi, ci diamo un gancio alle 8?\" (\"aizu, ¿8etan kakoa emango?\") -zera esan nahi du: \n \"-aizu, ¿8retan elkartuko gara?\". \n\"Darsi un gancio\" hitzordu bat lotzea da, X orduan eta Y lekuan.</p><p><ul><li>¿Zein ordutan da <i>gancio</i>-a manira joateko?</li><li>Ez dakit ta, dena den, ezin naiz joan <i>gancio</i>-a baitaukat bilera baterako.</li></ul></p>\n<h5>Kontaktuak</h5><p>Gancio-ko interfaze berria garatu duzula? Gancio-a abiatu nahi duzula zure hirian? Hobetzeko zerbait bururatu zaizu? Ba, jakin iturri-kodea askea dela eta <a href='https://git.lattuga.net/cisti/gancio'>hemen</a> dagoela eskuragarri. \nLaguntza eta iradokizunak beti direnez ongietorriak, gurekin kontaktuan jarri zaitezkete underscore@autistici.org-en. Ondo izan! </p><p>*Bilboaldea Bilbo, Ezkerraldea, Meatzaldea, Hego Uribe, Uribe Kosta eta Txorierri</p>", "about": "\n <p><a href='https://gancio.org'>Gancio</a> Tokiko komunitateentzako agenda partekatua da.</p>\n ",
"oauth": { "oauth": {
"authorization_request": "<code>{app}</code> aplikazioak baimena eskatu du <code>{instance_name}</code>-n ondorengo lanak egiteko:", "authorization_request": "<code>{app}</code> aplikazioak baimena eskatu du <code>{instance_name}</code>-n ondorengo lanak egiteko:",
"redirected_to": "Baieztapenaren ondoren <code>{url}</code> helbidera berbideratua izango zara.", "redirected_to": "Baieztapenaren ondoren <code>{url}</code> helbidera berbideratua izango zara",
"scopes": { "scopes": {
"event:write": "Zure ekitaldiak sortu eta aldatu" "event:write": "Zure ekitaldiak sortu eta aldatu"
} }
},
"validators": {
"email": "Sar ezazu posta elektroniko baliozko bat",
"required": "{fieldName} beharrezkoa da"
} }
} }

View file

@ -84,7 +84,9 @@
"address": "Adresse", "address": "Adresse",
"where": "Où", "where": "Où",
"send": "Envoyer", "send": "Envoyer",
"export": "Exporter" "export": "Exporter",
"label": "Nom",
"max_events": "Nb. max d'événements"
}, },
"event": { "event": {
"follow_me_description": "Une des manières de rester informé sur les évènements publiés ici sur {title}\nest de suivre le compte <u>{account}</u> sur le fediverse, par exemple via Mastodon, et pourquoi pas d'ajouter des ressources à un évènement à partir de là.<br/><br/>\nSi vous n'avez jamais entendu parler de Mastodon and du fediverse, nous vous recommandons de lire <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>cet article (en anglais)</a>.<br/><br/>Saisissez votre nom d'instance ci-dessous (par ex. mastodon.social)", "follow_me_description": "Une des manières de rester informé sur les évènements publiés ici sur {title}\nest de suivre le compte <u>{account}</u> sur le fediverse, par exemple via Mastodon, et pourquoi pas d'ajouter des ressources à un évènement à partir de là.<br/><br/>\nSi vous n'avez jamais entendu parler de Mastodon and du fediverse, nous vous recommandons de lire <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>cet article (en anglais)</a>.<br/><br/>Saisissez votre nom d'instance ci-dessous (par ex. mastodon.social)",
@ -129,7 +131,9 @@
"recurrent_2m_days": "|Le {days} un mois sur deux|Les {jours} un mois sur deux", "recurrent_2m_days": "|Le {days} un mois sur deux|Les {jours} un mois sur deux",
"recurrent_2w_days": "Un {days} sur deux", "recurrent_2w_days": "Un {days} sur deux",
"edit_recurrent": "Modifier lévènement récurrent :", "edit_recurrent": "Modifier lévènement récurrent :",
"updated": "Évènement mis à jour" "updated": "Évènement mis à jour",
"import_description": "Vous pouvez importer des événements depuis d'autres plateformes ou d'autres instances à travers des formats standards (ics et h-event)",
"saved": "Événement enregistré"
}, },
"register": { "register": {
"description": "Les mouvements sociaux doivent s'organiser et s'autofinancer.<br/>\n<br/>Avant de pouvoir publier, <strong> le compte doit être approuvé</strong>, considérez que <strong> derrière ce site vous trouverez de vraies personnes, à qui vous pouvez écrire en deux lignes pour exprimer les évènements que vous souhaiteriez publier.", "description": "Les mouvements sociaux doivent s'organiser et s'autofinancer.<br/>\n<br/>Avant de pouvoir publier, <strong> le compte doit être approuvé</strong>, considérez que <strong> derrière ce site vous trouverez de vraies personnes, à qui vous pouvez écrire en deux lignes pour exprimer les évènements que vous souhaiteriez publier.",
@ -169,7 +173,7 @@
"announcement_description": "Dans cette section vous pouvez insérer des annonces qui resteront affichées sur la page d'accueil", "announcement_description": "Dans cette section vous pouvez insérer des annonces qui resteront affichées sur la page d'accueil",
"announcement_remove_ok": "Annonce supprimée", "announcement_remove_ok": "Annonce supprimée",
"delete_announcement_confirm": "Êtes-vous sûr·e de vouloir supprimer l'annonce ?", "delete_announcement_confirm": "Êtes-vous sûr·e de vouloir supprimer l'annonce ?",
"user_block_confirm": "Êtes-vous sûr·e de vouloir bloquer cet utilisateur ?", "user_block_confirm": "Êtes-vous sûr·e de vouloir bloquer l'utilisateur {user} ?",
"favicon": "Logo", "favicon": "Logo",
"user_blocked": "Utilisateur {user} bloqué", "user_blocked": "Utilisateur {user} bloqué",
"resources": "Ressources", "resources": "Ressources",
@ -196,11 +200,20 @@
"allow_registration_description": "Autoriser l'ouverture des inscriptions ?", "allow_registration_description": "Autoriser l'ouverture des inscriptions ?",
"user_create_ok": "Utilisateur créé", "user_create_ok": "Utilisateur créé",
"user_remove_ok": "Utilisateur supprimé", "user_remove_ok": "Utilisateur supprimé",
"delete_user_confirm": "Êtes-vous sûr·e de vouloir supprimer cet administrateur ?", "delete_user_confirm": "Êtes-vous sûr·e de vouloir supprimer {user} ?",
"remove_admin": "Supprimer l'administrateur", "remove_admin": "Supprimer l'administrateur",
"delete_user": "Supprimer", "delete_user": "Supprimer",
"event_confirm_description": "Vous pouvez confirmer les évènements ajoutés par des utilisateurs anonymes ici", "event_confirm_description": "Vous pouvez confirmer les évènements ajoutés par des utilisateurs anonymes ici",
"place_description": "Si vous avez donné le mauvais lieu ou la mauvaise adresse, vous pouvez les modifier.<br/>Tous les évènements courants et passés associés à ce lieu seront mis à jour." "place_description": "Si vous avez donné le mauvais lieu ou la mauvaise adresse, vous pouvez les modifier.<br/>Tous les évènements courants et passés associés à ce lieu seront mis à jour.",
"add_instance": "Ajouter une instance",
"instance_block_confirm": "Êtes-vous sûr·e de vouloir bloquer l'instance {instance} ?",
"disable_user_confirm": "Êtes-vous sûr·e de vouloir désactiver {user} ?",
"smtp_hostname": "Nom d'hôte SMTP",
"smtp_test_button": "Envoyer un e-mail de test",
"smtp_description": "<ul><li>L'administrateur reçoit un e-mail lorsqu'un événement anonyme est ajouté (si activé).</li><li>L'administrateur reçoit un e-mail pour chaque demande d'inscription (si activé).</li><li>L'utilisateur reçoit un e-mail suite à sa demande d'inscription.</li><li>L'utilisateur reçoit un e-mail lorsque son inscription est confirmée.</li><li>L'utilisateur reçoit un e-mail de confirmation s'il est inscrit directement par l'administrateur.</li><li>Les utilisateurs reçoivent un e-mail pour restaurer leur mot de passe s'ils l'oublient.</li></ul>",
"show_smtp_setup": "Paramètres d'e-mail",
"smtp_test_success": "Un e-mail de test a été envoyé à {admin_email}, veuillez vérifier votre boîte de réception",
"admin_email": "E-mail de l'administrateur"
}, },
"oauth": { "oauth": {
"scopes": { "scopes": {
@ -254,5 +267,11 @@
"not_registered": "Pas encore inscrit·e ?", "not_registered": "Pas encore inscrit·e ?",
"check_email": "Vérifiez votre boîte de réception et les indésirables.", "check_email": "Vérifiez votre boîte de réception et les indésirables.",
"description": "En vous connectant vous pouvez publier de nouveaux évènements." "description": "En vous connectant vous pouvez publier de nouveaux évènements."
},
"setup": {
"check_db": "Vérifier la base de données",
"completed": "Configuration terminée",
"completed_description": "<p>Vous pouvez désormais vous connectez avec le compte utilisateur suivant :<br/><br/>Identifiant : <b>{email}</b><br/>Mot de passe : <b>{password}<b/></p>",
"start": "Commencer"
} }
} }

View file

@ -129,7 +129,7 @@
"where_description": "Dov'è il gancio? Se il posto non è presente potrai crearlo.", "where_description": "Dov'è il gancio? Se il posto non è presente potrai crearlo.",
"confirmed": "Evento confermato", "confirmed": "Evento confermato",
"not_found": "Evento non trovato", "not_found": "Evento non trovato",
"remove_confirmation": "Sei sicuro/a di voler eliminare questo evento?", "remove_confirmation": "Vuoi eliminare questo evento?",
"remove_recurrent_confirmation": "Sei sicura di voler eliminare questo evento ricorrente?\nGli eventi passati verranno mantenuti ma non ne verranno creati altri.", "remove_recurrent_confirmation": "Sei sicura di voler eliminare questo evento ricorrente?\nGli eventi passati verranno mantenuti ma non ne verranno creati altri.",
"recurrent": "Ricorrente", "recurrent": "Ricorrente",
"edit_recurrent": "Modifica evento ricorrente:", "edit_recurrent": "Modifica evento ricorrente:",
@ -159,7 +159,10 @@
"import_ICS": "Importa da ICS", "import_ICS": "Importa da ICS",
"import_URL": "Importa da URL (ics o h-event)", "import_URL": "Importa da URL (ics o h-event)",
"ics": "ICS", "ics": "ICS",
"import_description": "Puoi importare eventi da altre piattaforme e da altre istanze attraverso i formati standard (ics e h-event)" "import_description": "Puoi importare eventi da altre piattaforme e da altre istanze attraverso i formati standard (ics e h-event)",
"alt_text_description": "Descrizione per utenti con disabilità visive",
"choose_focal_point": "Scegli il punto centrale cliccando",
"remove_media_confirmation": "Confermi l'eliminazione dell'immagine?"
}, },
"admin": { "admin": {
"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).", "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).",
@ -191,15 +194,16 @@
"hide_resource": "Nascondi risorsa", "hide_resource": "Nascondi risorsa",
"show_resource": "Mostra risorsa", "show_resource": "Mostra risorsa",
"delete_resource": "Elimina risorsa", "delete_resource": "Elimina risorsa",
"delete_resource_confirm": "Sei sicuro/a di voler eliminare questa risorsa?", "delete_resource_confirm": "Sei sicurǝ di voler eliminare questa risorsa?",
"block_user": "Blocca questo utente", "block_user": "Blocca questo utente",
"user_blocked": "L'utente {user} non potrà più aggiungere risorse", "user_blocked": "L'utente {user} non potrà più aggiungere risorse",
"filter_instances": "Filtra istanze", "filter_instances": "Filtra istanze",
"filter_users": "Filtra utenti", "filter_users": "Filtra utenti",
"instance_name": "Nome istanza", "instance_name": "Nome istanza",
"favicon": "Logo", "favicon": "Logo",
"user_block_confirm": "Sei sicuro/a di voler bloccare l'utente?", "user_block_confirm": "Confermi di voler bloccare l'utente {user}?",
"delete_announcement_confirm": "Sei sicuro/a di voler eliminare l'annuncio?", "instance_block_confirm": "Confermi di voler bloccare l'istanza {instance}?",
"delete_announcement_confirm": "Vuoi eliminare questo l'annuncio?",
"announcement_remove_ok": "Annuncio rimosso", "announcement_remove_ok": "Annuncio rimosso",
"announcement_description": "In questa sezione puoi inserire annunci che rimarranno in homepage", "announcement_description": "In questa sezione puoi inserire annunci che rimarranno in homepage",
"instance_locale": "Lingua predefinita", "instance_locale": "Lingua predefinita",
@ -216,9 +220,10 @@
"is_dark": "Tema scuro", "is_dark": "Tema scuro",
"add_link": "Aggiungi link", "add_link": "Aggiungi link",
"footer_links": "Collegamenti del piè di pagina", "footer_links": "Collegamenti del piè di pagina",
"delete_footer_link_confirm": "Sei sicuro/a di eliminare questo collegamento?", "delete_footer_link_confirm": "Vuoi eliminare questo collegamento?",
"edit_place": "Modifica luogo", "edit_place": "Modifica luogo",
"new_announcement": "Nuovo annuncio" "new_announcement": "Nuovo annuncio",
"show_smtp_setup": "Impostazioni email"
}, },
"auth": { "auth": {
"not_confirmed": "Non ancora confermato…", "not_confirmed": "Non ancora confermato…",
@ -260,5 +265,10 @@
"scopes": { "scopes": {
"event:write": "Pubblicare/modificare i tuoi eventi" "event:write": "Pubblicare/modificare i tuoi eventi"
} }
},
"setup": {
"completed": "Setup completato",
"completed_description": "<p>Puoi entrare con le seguenti credenziali:<br/><br/>Utente: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
"start": "Inizia"
} }
} }

11
middleware/setup.js Normal file
View file

@ -0,0 +1,11 @@
export default function ({ req, redirect, route }) {
if (process.server) {
if (req.firstrun && route.path !== '/setup') {
return redirect('/setup')
}
if (!req.firstrun && route.path === '/setup') {
return redirect('/')
}
}
}

View file

@ -1,4 +1,4 @@
const conf = require('config') const config = require('./server/config.js')
module.exports = { module.exports = {
telemetry: false, telemetry: false,
@ -11,22 +11,30 @@ module.exports = {
{ charset: 'utf-8' }, { charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' } { name: 'viewport', content: 'width=device-width, initial-scale=1' }
], ],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }] script: [{ src: '/gancio-events.es.js' }],
link: [{ rel: 'icon', type: 'image/png', href: '/logo.png' }]
}, },
dev: (process.env.NODE_ENV !== 'production'), dev: (process.env.NODE_ENV !== 'production'),
server: config.server,
server: conf.server,
vue: {
config: {
ignoredElements: ['gancio-events']
}
},
/* /*
** Customize the progress-bar color ** Customize the progress-bar component
*/ */
loading: '~/components/Loading.vue', loading: '~/components/Loading.vue',
/* /*
** Global CSS ** Global CSS
*/ */
css: [ css: [
'@/assets/style.less', 'vuetify/dist/vuetify.min.css',
'@mdi/font/css/materialdesignicons.css' '@mdi/font/css/materialdesignicons.css',
'@/assets/style.less'
], ],
/* /*
@ -35,31 +43,25 @@ module.exports = {
plugins: [ plugins: [
'@/plugins/i18n.js', '@/plugins/i18n.js',
'@/plugins/filters', // text filters, datetime filters, generic transformation helpers etc. '@/plugins/filters', // text filters, datetime filters, generic transformation helpers etc.
'@/plugins/vue-clipboard', // vuetify '@/plugins/vuetify', // vuetify
'@/plugins/axios', // axios baseurl configuration '@/plugins/axios', // axios baseurl configuration
'@/plugins/validators', // inject validators '@/plugins/validators', // inject validators
'@/plugins/api', // api helpers '@/plugins/api', // api helpers
{ src: '@/plugins/v-calendar', ssr: false } // v-calendar { src: '@/plugins/v-calendar', ssr: false } // v-calendar
], ],
render: {
compressor: false,
bundleRenderer: {
shouldPreload: (file, type) => {
return ['script', 'style', 'font'].includes(type)
}
}
},
/* /*
** Nuxt.js modules ** Nuxt.js modules
*/ */
modules: [ modules: [
// Doc: https://axios.nuxtjs.org/usage // Doc: https://axios.nuxtjs.org/usage
'./@nuxtjs/axios', '@nuxtjs/axios',
'./@nuxtjs/auth', '@nuxtjs/auth',
['nuxt-express-module', { expressPath: 'server/', routesPath: 'server/routes' }] '@/server/initialize.server.js'
], ],
serverMiddleware: ['server/routes'],
/* /*
** Axios module configuration ** Axios module configuration
* See https://github.com/nuxt-community/axios-module#options * See https://github.com/nuxt-community/axios-module#options
@ -93,28 +95,9 @@ module.exports = {
} }
} }
}, },
buildModules: [
'@nuxtjs/vuetify'
],
vuetify: {
defaultAssets: false,
optionsPath: './vuetify.options.js',
treeShake: true
/* module options */
},
/*
** Build configuration
*/
build: { build: {
presets: ['@nuxt/babel-preset-app', { corejs: 3,
useBuiltIns: 'usage', // or "entry" cache: true,
corejs: 3 hardSource: true
}], },
babel: {
plugins: [['@babel/plugin-proposal-private-methods', { loose: true }]]
},
cache: true
}
} }

View file

@ -1,19 +1,17 @@
{ {
"name": "gancio", "name": "gancio",
"version": "1.0.0-alpha", "version": "1.2.2",
"description": "A shared agenda for local communities", "description": "A shared agenda for local communities",
"author": "lesion", "author": "lesion",
"scripts": { "scripts": {
"build": "cross-env nuxt build --modern", "build": "nuxt build --modern",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .", "start:inspect": "NODE_ENV=production node --inspect node_modules/.bin/nuxt start --modern",
"dev": "NODE_ENV=development node server/index.js", "dev": "nuxt dev",
"dev:nuxt": "cross-env NODE_ENV=development nuxt dev --modern", "start": "nuxt start --modern",
"doc": "cd docs && bundle exec jekyll b", "doc": "cd docs && bundle exec jekyll b",
"doc:dev": "cd docs && bundle exec jekyll s --drafts", "doc:dev": "cd docs && bundle exec jekyll s --drafts",
"migrate": "NODE_ENV=production sequelize db:migrate", "migrate": "NODE_ENV=production sequelize db:migrate",
"migrate:dev": "sequelize db:migrate", "migrate:dev": "sequelize db:migrate"
"start:debug": "cross-env DEBUG=* NODE_ENV=production node server/cli.js",
"start": "NODE_ENV=production node server/cli.js"
}, },
"files": [ "files": [
"server/", "server/",
@ -24,113 +22,76 @@
"locales/email/", "locales/email/",
"locales/", "locales/",
"store/", "store/",
"config/default.json",
"config/production.js",
".nuxt/", ".nuxt/",
"yarn.lock" "yarn.lock"
], ],
"dependencies": { "dependencies": {
"@nuxtjs/auth": "^4.9.1", "@nuxtjs/auth": "^4.9.1",
"@nuxtjs/axios": "^5.13.5", "@nuxtjs/axios": "^5.13.5",
"@popperjs/core": "2.9.2",
"accept-language": "^3.0.18", "accept-language": "^3.0.18",
"axios": "^0.21.1", "axios": "^0.24.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"bufferutil": "^4.0.1", "cookie-parser": "^1.4.6",
"config": "^3.3.6",
"consola": "^2.15.3",
"cookie-parser": "^1.4.5",
"core-js": "3.14.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-env": "^7.0.3", "dayjs": "^1.10.7",
"date-fns": "^2.21.3", "dompurify": "^2.3.3",
"dayjs": "^1.10.5", "email-templates": "^8.0.8",
"dompurify": "^2.2.9",
"email-templates": "^8.0.7",
"express": "^4.17.1", "express": "^4.17.1",
"express-oauth-server": "^2.0.0", "express-oauth-server": "lesion/express-oauth-server#master",
"express-prom-bundle": "^6.3.4", "http-signature": "^1.3.6",
"fs": "^0.0.1-security",
"global": "^4.4.0",
"http-signature": "^1.3.5",
"ical.js": "^1.4.0", "ical.js": "^1.4.0",
"ics": "^2.27.0", "ics": "^2.35.0",
"inquirer": "^8.1.1", "jsdom": "^18.1.1",
"jsdom": "^16.6.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"less": "^4.1.1", "linkify-html": "^3.0.4",
"linkifyjs": "3.0.0-beta.3", "linkifyjs": "3.0.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"microformat-node": "^2.0.1", "microformat-node": "^2.0.1",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"multer": "^1.4.2", "multer": "^1.4.3",
"nuxt": "^2.15.7", "nuxt-edge": "^2.16.0-27305297.ab1c6cb4",
"nuxt-express-module": "^0.0.11",
"pg": "^8.6.0", "pg": "^8.6.0",
"pg-native": "3.0.0", "sequelize": "^6.12.0-alpha.1",
"prom-client": "^13.1.0", "sequelize-slugify": "^1.6.0",
"sequelize": "^6.6.2", "sharp": "^0.27.2",
"sequelize-cli": "^6.2.0", "sqlite3": "mapbox/node-sqlite3#918052b",
"sequelize-slugify": "^1.5.0",
"sharp": "^0.28.2",
"sqlite3": "^5.0.2",
"tiptap": "^1.32.0", "tiptap": "^1.32.0",
"tiptap-extensions": "^1.35.0", "tiptap-extensions": "^1.35.0",
"to-ico": "^1.1.5", "umzug": "^2.3.0",
"url": "^0.11.0", "v-calendar": "2.3.4",
"utf-8-validate": "^5.0.5",
"v-calendar": "2.3.0",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-clipboard2": "^0.3.1", "vue-i18n": "^8.26.7",
"vue-i18n": "^8.24.4",
"vue-server-renderer": "^2.6.14",
"vue-template-compiler": "^2.6.14", "vue-template-compiler": "^2.6.14",
"vuetify": "^2.6.1",
"winston": "^3.3.3", "winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.5", "winston-daily-rotate-file": "^4.5.5",
"yargs": "^17.0.1" "yargs": "^17.2.0"
}, },
"devDependencies": { "devDependencies": {
"@mdi/font": "^5.9.55", "@mdi/font": "^6.5.95",
"@nuxtjs/eslint-config": "^6.0.1", "less": "^4.1.1",
"@nuxtjs/vuetify": "^1.12.1", "less-loader": "^7",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.27.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": ">=11.1.0",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^7.10.0",
"fibers": "^5.0.0",
"less-loader": "7",
"prettier": "^2.3.0", "prettier": "^2.3.0",
"pug": "^3.0.2", "pug": "^3.0.2",
"pug-plain-loader": "^1.1.0", "pug-plain-loader": "^1.1.0",
"sass": "^1.32.12", "sass": "^1.43.5",
"sass-loader": "10", "sequelize-cli": "^6.3.0",
"typescript": "^4.3.4",
"vue-cli-plugin-vuetify": "~2.4.0",
"vuetify": "^2.5.4",
"vuetify-loader": "^1.7.1",
"webpack": "4", "webpack": "4",
"webpack-cli": "^4.7.2" "webpack-cli": "^4.7.2"
}, },
"resolutions": { "resolutions": {
"prosemirror-model": "1.14.1",
"source-map-resolve": "0.6.0", "source-map-resolve": "0.6.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"minimist": "1.2.5", "minimist": "1.2.5",
"jimp": "0.16.1", "jimp": "0.16.1",
"resize-img": "2.0.0", "resize-img": "2.0.0",
"underscore": "1.13.1", "underscore": "1.13.1",
"@nuxtjs/vuetify/**/sass": "1.32.12" "@nuxtjs/vuetify/**/sass": "1.32.12",
"postcss": "7.0.36",
"glob-parent": "5.1.2",
"chokidar": "3.5.2",
"core-js": "3.19.0"
}, },
"bin": { "bin": {
"gancio": "server/cli.js" "gancio": "server/cli.js"

View file

@ -1,19 +1,18 @@
<template lang='pug'> <template lang='pug'>
v-row.mt-5(align='center' justify='center') .d-flex.justify-space-around
v-col(cols='12' md="6" lg="5" xl="4") v-card.mt-5(max-width='600px')
v-card(light) v-card-title {{settings.title}} - {{$t('common.authorize')}}
v-card-title {{settings.title}} - {{$t('common.authorize')}} v-card-text
v-card-text u {{$auth.user.email}}
u {{$auth.user.email}} div
div p(v-html="$t('oauth.authorization_request', { app: client.name, instance_name: settings.title })")
p(v-html="$t('oauth.authorization_request', { app: client.name, instance_name: settings.title })") ul.mb-2
ul li(v-for="s in scope.split(' ')") {{$t(`oauth.scopes.${scope}`)}}
li(v-for="s in scope.split(' ')") {{$t(`oauth.scopes.${scope}`)}} span(v-html="$t('oauth.redirected_to', {url: $route.query.redirect_uri})")
span(v-html="$t('oauth.redirected_to', {url: $route.query.redirect_uri})") v-card-actions
v-card-actions v-spacer
v-spacer v-btn(color='error' to='/') {{$t('common.cancel')}}
v-btn(color='error' to='/') {{$t('common.cancel')}} v-btn(:href='authorizeURL' color='success') {{$t('common.authorize')}}
v-btn(:href='authorizeURL' color='success') {{$t('common.authorize')}}
</template> </template>
<script> <script>
@ -71,7 +70,7 @@ export default {
} }
} }
</script> </script>
<style lang='less'> <style>
h4 img { h4 img {
max-height: 40px; max-height: 40px;
border-radius: 20px; border-radius: 20px;

View file

@ -11,13 +11,13 @@
v-text-field(v-model='email' type='email' v-text-field(v-model='email' type='email'
validate-on-blur validate-on-blur
:rules='$validators.email' autofocus :rules='$validators.email' autofocus
:placeholder='$t("common.email")' :label='$t("common.email")'
ref='email') ref='email')
v-text-field(v-model='password' v-text-field(v-model='password'
:rules='$validators.password' :rules='$validators.password'
type='password' type='password'
:placeholder='$t("common.password")') :label='$t("common.password")')
v-card-actions v-card-actions
v-btn(text v-btn(text

View file

@ -7,8 +7,10 @@ v-col(cols=12)
v-btn(v-if='settings.allow_recurrent_event' value='recurrent' label="recurrent") {{$t('event.recurrent')}} v-btn(v-if='settings.allow_recurrent_event' value='recurrent' label="recurrent") {{$t('event.recurrent')}}
p {{$t(`event.${type}_description`)}} p {{$t(`event.${type}_description`)}}
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-if='type === "recurrent"' color='primary' :value='value.recurrent.frequency' @change='fq => change("frequency", fq)') v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-if='type === "recurrent"' color='primary' :value='value.recurrent.frequency' @change='fq => change("frequency", fq)')
v-btn(v-for='f in frequencies' :key='f.value' :value='f.value') {{f.text}} v-btn(v-for='f in frequencies' :key='f.value' :value='f.value') {{f.text}}
client-only client-only
.datePicker.mt-3 .datePicker.mt-3
v-input(:value='fromDate' v-input(:value='fromDate'
@ -43,11 +45,6 @@ v-col(cols=12)
:value='dueHour' clearable :value='dueHour' clearable
:items='hourList' @change='hr => change("dueHour", hr)') :items='hourList' @change='hr => change("dueHour", hr)')
//- div.col-md-12(v-if='isRecurrent')
//- p(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') 🡲 {{whenPatterns}}
//- v-btn-toggle(v-else dense group v-model='value.recurrent.type' color='primary')
//- v-btn(text link v-for='whenPattern in whenPatterns' :value='whenPattern.key' :key='whenPatterns.key') {{whenPattern.label}}
List(v-if='type==="normal" && todayEvents.length' :events='todayEvents' :title='$t("event.same_day")') List(v-if='type==="normal" && todayEvents.length' :events='todayEvents' :title='$t("event.same_day")')
</template> </template>
@ -61,17 +58,13 @@ export default {
name: 'DateInput', name: 'DateInput',
components: { List }, components: { List },
props: { props: {
value: { type: Object, default: () => ({ from: null, due: null, recurrent: null }) } value: { type: Object, default: () => ({ from: null, due: null, recurrent: null }) },
event: { type: Object, default: () => null }
}, },
data () { data () {
return { return {
type: 'normal', type: 'normal',
time: { start: null, end: null },
fromDateMenu: null,
dueDateMenu: null,
date: null,
page: null, page: null,
frequency: '',
events: [], events: [],
frequencies: [ frequencies: [
{ value: '1w', text: this.$t('event.each_week') }, { value: '1w', text: this.$t('event.each_week') },
@ -85,7 +78,7 @@ export default {
todayEvents () { todayEvents () {
const start = dayjs(this.value.from).startOf('day').unix() const start = dayjs(this.value.from).startOf('day').unix()
const end = dayjs(this.value.from).endOf('day').unix() const end = dayjs(this.value.from).endOf('day').unix()
const events = this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end) const events = this.events.filter(e => (this.event.id && e.id !== this.event.id) && e.start_datetime >= start && e.start_datetime <= end)
return events return events
}, },
attributes () { attributes () {
@ -106,16 +99,15 @@ export default {
}, },
hourList () { hourList () {
const hourList = [] const hourList = []
const pad = '00' const leftPad = h => ('00' + h).slice(-2)
for (let h = 0; h < 24; h++) { for (let h = 0; h < 24; h++) {
hourList.push(`${(pad + h).slice(-pad.length)}:00`) const textHour = leftPad(h < 13 ? h : h - 12)
hourList.push(`${(pad + h).slice(-pad.length)}:30`) hourList.push({ text: textHour + ':00 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':00' })
hourList.push({ text: textHour + ':30 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':30' })
} }
return hourList return hourList
}, },
isRecurrent () {
return !!this.value.recurrent
},
whenPatterns () { whenPatterns () {
if (!this.value.from) { return } if (!this.value.from) { return }
const date = dayjs(this.value.from) const date = dayjs(this.value.from)
@ -181,7 +173,7 @@ export default {
if (what === 'type') { if (what === 'type') {
if (typeof value === 'undefined') { this.type = 'normal' } if (typeof value === 'undefined') { this.type = 'normal' }
if (value === 'recurrent') { if (value === 'recurrent') {
this.$emit('input', { ...this.value, recurrent: {}, multidate: false }) this.$emit('input', { ...this.value, recurrent: { frequency: '1w' }, multidate: false })
} else if (value === 'multidate') { } else if (value === 'multidate') {
this.$emit('input', { ...this.value, recurrent: null, multidate: true }) this.$emit('input', { ...this.value, recurrent: null, multidate: true })
} else { } else {
@ -213,14 +205,14 @@ export default {
const fromHour = dayjs(this.value.from).hour() const fromHour = dayjs(this.value.from).hour()
// add a day // add a day
let due = dayjs(this.value.due) let due = dayjs(this.value.from)
if (fromHour > Number(hour) && !this.value.multidate) { if (fromHour > Number(hour) && !this.value.multidate) {
due = due.add(1, 'day') due = due.add(1, 'day')
} }
due = due.hour(hour).minute(minute) due = due.hour(hour).minute(minute)
this.$emit('input', { ...this.value, due, dueHour: true }) this.$emit('input', { ...this.value, due, dueHour: true })
} else { } else {
this.$emit('input', { ...this.value, dueHour: false }) this.$emit('input', { ...this.value, due: null, dueHour: false })
} }
// change date in calendar (could be a range or a recurrent event...) // change date in calendar (could be a range or a recurrent event...)
} else if (what === 'date') { } else if (what === 'date') {
@ -240,30 +232,22 @@ export default {
this.$emit('input', { ...this.value, from, due }) this.$emit('input', { ...this.value, from, due })
} else { } else {
let from = value let from = value
let due = value let due = this.value.due
if (this.value.fromHour) { if (this.value.fromHour) {
from = dayjs(value).hour(dayjs(this.value.from).hour()) from = dayjs(value).hour(dayjs(this.value.from).hour())
} }
if (this.value.dueHour) { if (this.value.dueHour && this.value.due) {
due = dayjs(value).hour(dayjs(this.value.due).hour()) due = dayjs(value).hour(dayjs(this.value.due).hour())
} }
this.$emit('input', { ...this.value, from, due }) this.$emit('input', { ...this.value, from, due })
} }
} }
}, },
changeType (type) {
if (type === 'recurrent') {
this.updateRecurrent({})
}
},
selectFrequency (f) {
this.$emit('input', { recurrent: { frequency: f }, from: this.value.from, due: this.value.due })
}
} }
} }
</script> </script>
<style lang="less"> <style>
.datePicker { .datePicker {
max-width: 500px !important; max-width: 500px !important;
margin: 0 auto; margin: 0 auto;

View file

@ -5,14 +5,14 @@
p(v-html="$t('event.import_description')") p(v-html="$t('event.import_description')")
v-form(v-model='valid' ref='form' lazy-validation @submit.prevent='importGeneric') v-form(v-model='valid' ref='form' lazy-validation @submit.prevent='importGeneric')
v-row v-row
v-col .col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12
v-text-field(v-model='URL' v-text-field(v-model='URL'
:label="$t('common.url')" :label="$t('common.url')"
:hint="$t('event.import_URL')" :hint="$t('event.import_URL')"
persistent-hint persistent-hint
:loading='loading' :error='error' :loading='loading' :error='error'
:error-messages='errorMessage') :error-messages='errorMessage')
v-col .col
v-file-input( v-file-input(
v-model='file' v-model='file'
accept=".ics" accept=".ics"
@ -22,8 +22,8 @@
v-card-actions v-card-actions
v-spacer v-spacer
v-btn(@click='$emit("close")' color='warning') {{$t('common.cancel')}} v-btn(text @click='$emit("close")' color='warning') {{$t('common.cancel')}}
v-btn(@click='importGeneric' :loading='loading' :disabled='loading' v-btn(text @click='importGeneric' :loading='loading' :disabled='loading'
color='primary') {{$t('common.import')}} color='primary') {{$t('common.import')}}
</template> </template>

175
pages/add/MediaInput.vue Normal file
View file

@ -0,0 +1,175 @@
<template lang="pug">
span
v-dialog(v-model='openMediaDetails' :fullscreen="$vuetify.breakpoint.xsOnly" width='1000px')
v-card
v-card-title {{$t('common.media')}}
v-card-text
v-row.mt-1
v-col#focalPointSelector(
@mousedown='handleStart' @touchstart='handleStart'
@mousemove='handleMove' @touchmove='handleMove'
@mouseup='handleStop' @touchend='handleStop'
)
div.focalPoint(:style="{ top, left }")
img(v-if='mediaPreview' :src='mediaPreview')
v-col.col-12.col-sm-4
p {{$t('event.choose_focal_point')}}
img.img.d-none.d-sm-block(v-if='mediaPreview'
:src='mediaPreview' :style="{ 'object-position': position }")
v-textarea.mt-4(type='text'
label='Alternative text'
persistent-hint
@input='v => name=v'
:value='value.name' filled
:hint='$t("event.alt_text_description")')
br
v-card-actions.justify-space-between
v-btn(text @click='openMediaDetails=false' color='warning') Cancel
v-btn(text color='primary' @click='save') Save
h3.mb-3.font-weight-regular(v-if='mediaPreview') {{$t('common.media')}}
v-card-actions(v-if='mediaPreview')
v-spacer
v-btn(text color='primary' @click='openMediaDetails = true') {{$t('common.edit')}}
v-btn(text color='error' @click='remove') {{$t('common.remove')}}
div(v-if='mediaPreview')
img.img.col-12.ml-3(:src='mediaPreview' :style="{ 'object-position': savedPosition }")
span.float-right {{event.media[0].name}}
v-file-input(
v-else
:label="$t('common.media')"
:hint="$t('event.media_description')"
prepend-icon="mdi-camera"
:value='value.image'
@change="selectMedia"
persistent-hint
accept='image/*')
</template>
<script>
export default {
name: 'MediaInput',
props: {
value: { type: Object, default: () => ({ image: null }) },
event: { type: Object, default: () => {} }
},
data () {
return {
openMediaDetails: false,
name: this.value.name || '',
focalpoint: this.value.focalpoint || [0, 0],
dragging: false
}
},
computed: {
mediaPreview () {
if (!this.value.url && !this.value.image) {
return false
}
const url = this.value.image ? URL.createObjectURL(this.value.image) : /^https?:\/\//.test(this.value.url) ? this.value.url : `/media/thumb/${this.value.url}`
return url
},
top () {
return ((this.focalpoint[1] + 1) * 50) + '%'
},
left () {
return ((this.focalpoint[0] + 1) * 50) + '%'
},
savedPosition () {
const focalpoint = this.value.focalpoint || [0, 0]
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
},
position () {
const focalpoint = this.focalpoint || [0, 0]
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
}
},
methods: {
save () {
this.$emit('input', { url: this.value.url, image: this.value.image, name: this.name || this.value.image.name || '', focalpoint: [...this.focalpoint] })
this.openMediaDetails = false
},
async remove () {
const ret = await this.$root.$confirm('event.remove_media_confirmation')
if (!ret) { return }
this.$emit('remove')
},
selectMedia (v) {
this.$emit('input', { image: v, name: v.name, focalpoint: [0, 0] })
},
handleStart (ev) {
ev.preventDefault()
this.dragging = true
this.handleMove(ev, true)
return false
},
handleStop (ev) {
this.dragging = false
},
handleMove (ev, manual = false) {
if (!this.dragging && !manual) return
ev.stopPropagation()
const boundingClientRect = document.getElementById('focalPointSelector').getBoundingClientRect()
const clientX = ev.changedTouches ? ev.changedTouches[0].clientX : ev.clientX
const clientY = ev.changedTouches ? ev.changedTouches[0].clientY : ev.clientY
// get relative coordinate
let x = Math.ceil(clientX - boundingClientRect.left)
let y = Math.ceil(clientY - boundingClientRect.top)
// snap to border
x = x < 30 ? 0 : x > boundingClientRect.width - 30 ? boundingClientRect.width : x
y = y < 30 ? 0 : y > boundingClientRect.height - 30 ? boundingClientRect.height : y
// this.relativeFocalpoint = [x + 'px', y + 'px']
// map to real image coordinate
const posY = -1 + (y / boundingClientRect.height) * 2
const posX = -1 + (x / boundingClientRect.width) * 2
this.focalpoint = [posX, posY]
return false
}
}
}
</script>
<style>
.cursorPointer {
cursor: crosshair;
}
.img {
width: 100%;
object-fit: cover;
object-position: top;
aspect-ratio: 1.7778;
}
#focalPointSelector {
position: relative;
cursor: move;
overflow: hidden;
display: flex;
height: 100%;
justify-self: center;
align-items: center;
}
#focalPointSelector img {
width: 100%;
}
.focalPoint {
position: absolute;
width: 50px;
height: 50px;
top: 100px;
left: 100px;
transform: translate(-25px, -25px);
border-radius: 50%;
border: 1px solid #ff6d408e;
box-shadow: 0 0 0 9999em rgba(0, 0, 0, .65);
}
</style>

View file

@ -6,7 +6,7 @@
v-spacer v-spacer
v-btn(link text color='primary' @click='openImportDialog=true') v-btn(link text color='primary' @click='openImportDialog=true')
<v-icon>mdi-file-import</v-icon> {{$t('common.import')}} <v-icon>mdi-file-import</v-icon> {{$t('common.import')}}
v-dialog(v-model='openImportDialog') v-dialog(v-model='openImportDialog' :fullscreen='$vuetify.breakpoint.xsOnly')
ImportDialog(@close='openImportDialog=false' @imported='eventImported') ImportDialog(@close='openImportDialog=false' @imported='eventImported')
v-card-text.px-0.px-xs-2 v-card-text.px-0.px-xs-2
@ -33,8 +33,7 @@
WhereInput(ref='where' v-model='event.place') WhereInput(ref='where' v-model='event.place')
//- When //- When
DateInput(v-model='date') DateInput(v-model='date' :event='event')
//- Description //- Description
v-col.px-0(cols='12') v-col.px-0(cols='12')
Editor.px-3.ma-0( Editor.px-3.ma-0(
@ -45,14 +44,7 @@
//- MEDIA / FLYER / POSTER //- MEDIA / FLYER / POSTER
v-col(cols=12 md=6) v-col(cols=12 md=6)
v-file-input( MediaInput(v-model='event.media[0]' :event='event' @remove='event.media=[]')
:label="$t('common.media')"
:hint="$t('event.media_description')"
prepend-icon="mdi-camera"
v-model='event.image'
persistent-hint
accept='image/*')
v-img.col-12.col-sm-2.ml-3(v-if='mediaPreview' :src='mediaPreview')
//- tags //- tags
v-col(cols=12 md=6) v-col(cols=12 md=6)
@ -66,7 +58,7 @@
v-card-actions v-card-actions
v-spacer v-spacer
v-btn(@click='done' :loading='loading' :disabled='!valid || loading' v-btn(@click='done' :loading='loading' :disabled='!valid || loading'
color='primary') {{edit?$t('common.edit'):$t('common.send')}} color='primary') {{edit?$t('common.save'):$t('common.send')}}
</template> </template>
<script> <script>
@ -77,16 +69,17 @@ import List from '@/components/List'
import ImportDialog from './ImportDialog' import ImportDialog from './ImportDialog'
import DateInput from './DateInput' import DateInput from './DateInput'
import WhereInput from './WhereInput' import WhereInput from './WhereInput'
import MediaInput from './MediaInput'
export default { export default {
name: 'NewEvent', name: 'NewEvent',
components: { List, Editor, ImportDialog, WhereInput, DateInput }, components: { List, Editor, ImportDialog, MediaInput, WhereInput, DateInput },
validate ({ store }) { validate ({ store }) {
return (store.state.auth.loggedIn || store.state.settings.allow_anon_event) return (store.state.auth.loggedIn || store.state.settings.allow_anon_event)
}, },
async asyncData ({ params, $axios, error, store }) { async asyncData ({ params, $axios, error, store }) {
if (params.edit) { if (params.edit) {
const data = { event: { place: {} } } const data = { event: { place: {}, media: [] } }
data.id = params.edit data.id = params.edit
data.edit = true data.edit = true
let event let event
@ -112,7 +105,7 @@ export default {
data.event.description = event.description data.event.description = event.description
data.event.id = event.id data.event.id = event.id
data.event.tags = event.tags data.event.tags = event.tags
data.event.image_path = event.image_path data.event.media = event.media || []
return data return data
} }
return {} return {}
@ -128,15 +121,14 @@ export default {
title: '', title: '',
description: '', description: '',
tags: [], tags: [],
image: null media: []
}, },
page: { month, year }, page: { month, year },
fileList: [], fileList: [],
id: null, id: null,
date: { from: 0, due: 0, recurrent: null }, date: { from: null, due: null, recurrent: null },
edit: false, edit: false,
loading: false, loading: false,
mediaUrl: '',
disableAddress: false disableAddress: false
} }
}, },
@ -145,16 +137,7 @@ export default {
title: `${this.settings.title} - ${this.$t('common.add_event')}` title: `${this.settings.title} - ${this.$t('common.add_event')}`
} }
}, },
computed: { computed: mapState(['tags', 'places', 'settings']),
...mapState(['tags', 'places', 'settings']),
mediaPreview () {
if (!this.event.image && !this.event.image_path) {
return false
}
const url = this.event.image ? URL.createObjectURL(this.event.image) : `/media/thumb/${this.event.image_path}`
return url
}
},
methods: { methods: {
...mapActions(['updateMeta']), ...mapActions(['updateMeta']),
eventImported (event) { eventImported (event) {
@ -170,9 +153,6 @@ export default {
} }
this.openImportDialog = false this.openImportDialog = false
}, },
cleanFile () {
this.event.image = {}
},
async done () { async done () {
if (!this.$refs.form.validate()) { if (!this.$refs.form.validate()) {
this.$nextTick(() => { this.$nextTick(() => {
@ -187,16 +167,20 @@ export default {
formData.append('recurrent', JSON.stringify(this.date.recurrent)) formData.append('recurrent', JSON.stringify(this.date.recurrent))
if (this.event.image) { if (this.event.media.length) {
formData.append('image', this.event.image) formData.append('image', this.event.media[0].image)
formData.append('image_url', this.event.media[0].url)
formData.append('image_name', this.event.media[0].name)
formData.append('image_focalpoint', this.event.media[0].focalpoint)
} }
formData.append('title', this.event.title) formData.append('title', this.event.title)
formData.append('place_name', this.event.place.name) formData.append('place_name', this.event.place.name)
formData.append('place_address', this.event.place.address) formData.append('place_address', this.event.place.address)
formData.append('description', this.event.description) formData.append('description', this.event.description)
formData.append('multidate', !!this.date.multidate) formData.append('multidate', !!this.date.multidate)
formData.append('start_datetime', dayjs(this.date.from).unix()) formData.append('start_datetime', dayjs(this.date.from).unix())
formData.append('end_datetime', this.date.due && dayjs(this.date.due).unix()) formData.append('end_datetime', this.date.due ? dayjs(this.date.due).unix() : this.date.from.add(2, 'hour').unix())
if (this.edit) { if (this.edit) {
formData.append('id', this.event.id) formData.append('id', this.event.id)
@ -219,7 +203,7 @@ export default {
this.$root.$message('event.image_too_big', { color: 'error' }) this.$root.$message('event.image_too_big', { color: 'error' })
break break
default: default:
this.$root.$message(e.response.data, { color: 'error' }) this.$root.$message(e.response ? e.response.data : e, { color: 'error' })
} }
this.loading = false this.loading = false
} }

View file

@ -1,8 +1,8 @@
<template lang="pug"> <template lang="pug">
nuxt-link.embed_event(:to='`/event/${event.slug || event.id}`' target='_blank' :class='{ withImg: event.image_path }') nuxt-link.embed_event(:to='`/event/${event.slug || event.id}`' target='_blank' :class='{ withImg: event.media }')
//- image //- image
img.float-left(:src='`/media/thumb/${event.image_path || "logo.png"}`') img.float-left(:src='event | mediaURL("thumb")')
.event-info .event-info
//- title //- title
.date {{event|when}}<br/> .date {{event|when}}<br/>
@ -37,7 +37,7 @@ export default {
.embed_event { .embed_event {
display: flex; display: flex;
transition: margin .1s; transition: margin .1s;
background: url('/favicon.ico') no-repeat right 5px bottom 5px; background: url('/logo.png') no-repeat right 5px bottom 5px;
background-size: 32px; background-size: 32px;
background-color: #1f1f1f; background-color: #1f1f1f;
text-decoration: none; text-decoration: none;

View file

@ -11,10 +11,12 @@ export default {
const title = query.title const title = query.title
const tags = query.tags const tags = query.tags
const places = query.places const places = query.places
const show_recurrent = !!query.show_recurrent
let params = [] let params = []
if (places) { params.push(`places=${places}`) } if (places) { params.push(`places=${places}`) }
if (tags) { params.push(`tags=${tags}`) } if (tags) { params.push(`tags=${tags}`) }
if (show_recurrent) { params.push('show_recurrent=1') }
params = params.length ? `?${params.join('&')}` : '' params = params.length ? `?${params.join('&')}` : ''
const events = await $axios.$get(`/export/json${params}`) const events = await $axios.$get(`/export/json${params}`)

View file

@ -11,13 +11,14 @@ v-container#event.pa-0.pa-sm-2
v-row v-row
v-col.col-12.col-lg-8 v-col.col-12.col-lg-8
//- fake image to use u-featured in h-event microformat //- fake image to use u-featured in h-event microformat
img.u-featured(v-show='false' :src='`${settings.baseurl}${imgPath}`') img.u-featured(v-show='false' v-if='hasMedia' :src='event | mediaURL')
v-img.main_image.mb-3( v-img.main_image.mb-3(
contain contain
:src='imgPath' :alt='event | mediaURL("alt")'
:lazy-src='thumbImgPath' :src='event | mediaURL'
v-if='event.image_path') :lazy-src='event | mediaURL("thumb")'
.p-description.text-body-1.pa-3.grey.darken-4.rounded(v-else v-html='event.description') v-if='hasMedia')
.p-description.text-body-1.pa-3.rounded(v-if='!hasMedia && event.description' v-html='event.description')
v-col.col-12.col-lg-4 v-col.col-12.col-lg-4
v-card v-card
@ -34,21 +35,20 @@ v-container#event.pa-0.pa-sm-2
.text-h6.p-location .text-h6.p-location
v-icon mdi-map-marker v-icon mdi-map-marker
b.vcard.ml-2 {{event.place.name}} b.vcard.ml-2 {{event.place && event.place.name}}
.text-subtitle-1.adr {{event.place.address}} .text-subtitle-1.adr {{event.place && event.place.address}}
//- tags, hashtags //- tags, hashtags
v-card-text(v-if='event.tags.length') v-card-text(v-if='event.tags.length')
v-chip.p-category.ml-1.mt-3(v-for='tag in event.tags' color='primary' v-chip.p-category.ml-1.mt-3(v-for='tag in event.tags' color='primary'
outlined :key='tag' v-text='tag') outlined :key='tag')
span(v-text='tag')
//- info & actions //- info & actions
v-toolbar v-toolbar
v-tooltip(bottom) {{$t('common.copy_link')}} v-tooltip(bottom) {{$t('common.copy_link')}}
template(v-slot:activator="{on, attrs} ") template(v-slot:activator="{on, attrs} ")
v-btn.ml-2(large icon v-on='on' color='primary' v-btn.ml-2(large icon v-on='on' color='primary' @click='clipboard(`${settings.baseurl}/event/${event.slug || event.id}`)')
v-clipboard:success='copyLink'
v-clipboard:copy='`${settings.baseurl}/event/${event.slug || event.id}`')
v-icon mdi-content-copy v-icon mdi-content-copy
v-tooltip(bottom) {{$t('common.embed')}} v-tooltip(bottom) {{$t('common.embed')}}
template(v-slot:activator="{on, attrs} ") template(v-slot:activator="{on, attrs} ")
@ -60,35 +60,43 @@ v-container#event.pa-0.pa-sm-2
:href='`/api/event/${event.slug || event.id}.ics`') :href='`/api/event/${event.slug || event.id}.ics`')
v-icon mdi-calendar-export v-icon mdi-calendar-export
.p-description.text-body-1.pa-3.grey.darken-4.rounded(v-if='event.image_path && event.description' v-html='event.description') .p-description.text-body-1.pa-3.rounded(v-if='hasMedia && event.description' v-html='event.description')
//- resources from fediverse //- resources from fediverse
#resources.mt-1(v-if='settings.enable_federation') #resources.mt-1(v-if='settings.enable_federation')
div.float-right(v-if='!settings.hide_boosts') //- div.float-right(v-if='settings.hide_boosts')
small.mr-3 🔖 {{event.likes.length}} //- small.mr-3 🔖 {{event.likes.length}}
small {{event.boost.length}}<br/> //- small {{event.boost.length}}<br/>
v-dialog.showResource#resourceDialog(v-model='showResources' fullscreen v-dialog(v-model='showResources'
width='95vw' fullscreen
destroy-on-close destroy-on-close
@keydown.native.right='$refs.carousel.next()' scrollable
@keydown.native.left='$refs.carousel.prev()') transition='dialog-bottom-transition')
v-carousel(:interval='10000' ref='carousel' arrow='always') v-card
v-carousel-item(v-for='attachment in selectedResource.data.attachment' :key='attachment.url') v-btn.ma-2(icon dark @click='showResources = false')
v-img(:src='attachment.url') v-icon mdi-close
v-list.mb-1(v-if='settings.enable_resources' v-for='resource in event.resources' dark v-carousel.pa-5(:interval='10000' ref='carousel' hide-delimiters v-model='currentAttachment'
:key='resource.id' :class='{disabled: resource.hidden}') height='100%' show-arrows-on-over)
v-list-item v-carousel-item(v-for='attachment in selectedResource.data.attachment'
v-list-title v-if='isImg(attachment)'
:key='attachment.url')
v-img(:src='attachment.url' contain max-width='100%' max-height='100%')
v-card-actions.align-center.justify-center
span {{currentAttachmentLabel}}
v-card.grey.darken-4.mb-3#resources(v-if='settings.enable_resources' v-for='resource in event.resources'
:key='resource.id' :class='{disabled: resource.hidden}' elevation='10' outlined)
v-card-title
v-menu(v-if='$auth.user && $auth.user.is_admin' offset-y) v-menu(v-if='$auth.user && $auth.user.is_admin' offset-y)
template(v-slot:activator="{ on, attrs }") template(v-slot:activator="{ on }")
v-btn.mr-2(v-on='on' v-attrs='attrs' color='primary' small icon outlined) v-btn.mr-2(v-on='on' color='primary' small icon)
v-icon mdi-dots-vertical v-icon mdi-dots-vertical
v-list v-list
v-list-item(v-if='!resource.hidden' @click='hideResource(resource, true)') v-list-item(v-if='!resource.hidden' @click='hideResource(resource, true)')
v-list-item-title <v-icon left>mdi-eye-off</v-icon> {{$t('admin.hide_resource')}} v-list-item-title <v-icon left>mdi-eye-off</v-icon> {{$t('admin.hide_resource')}}
v-list-item(v-else @click='hideResource(resource, false)') v-list-item(v-else @click='hideResource(resource, false)')
v-list-item-title <v-icon left>mdi-eye-on</v-icon> {{$t('admin.show_resource')}} v-list-item-title <v-icon left>mdi-eye</v-icon> {{$t('admin.show_resource')}}
v-list-item(@click='deleteResource(resource)') v-list-item(@click='deleteResource(resource)')
v-list-item-title <v-icon left>mdi-delete</v-icon> {{$t('admin.delete_resource')}} v-list-item-title <v-icon left>mdi-delete</v-icon> {{$t('admin.delete_resource')}}
v-list-item(@click='blockUser(resource)') v-list-item(@click='blockUser(resource)')
@ -97,9 +105,16 @@ v-container#event.pa-0.pa-sm-2
a(:href='resource.data.url || resource.data.context') a(:href='resource.data.url || resource.data.context')
small {{resource.data.published|dateFormat('ddd, D MMMM HH:mm')}} small {{resource.data.published|dateFormat('ddd, D MMMM HH:mm')}}
v-card-text
div.mt-1(v-html='resource_filter(resource.data.content)') div.mt-1(v-html='resource_filter(resource.data.content)')
span.previewImage(@click='showResource(resource)') span(v-for='attachment in resource.data.attachment' :key='attachment.url')
img(v-for='img in resource.data.attachment' :src='img.url') audio(v-if='isAudio(attachment)' controls)
source(:src='attachment.url')
v-img.cursorPointer(v-if='isImg(attachment)' :src='attachment.url' @click='showResource(resource)'
max-height="250px"
max-width="250px"
contain :alt='attachment.name')
//- Next/prev arrow //- Next/prev arrow
.text-center.mt-5.mb-5 .text-center.mt-5.mb-5
@ -110,7 +125,7 @@ v-container#event.pa-0.pa-sm-2
:to='`/event/${event.next}`' :disabled='!event.next') :to='`/event/${event.next}`' :disabled='!event.next')
v-icon mdi-arrow-right v-icon mdi-arrow-right
v-dialog(v-model='showEmbed' width='1000px') v-dialog(v-model='showEmbed' width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
EmbedEvent(:event='event' @close='showEmbed=false') EmbedEvent(:event='event' @close='showEmbed=false')
</template> </template>
@ -118,22 +133,27 @@ v-container#event.pa-0.pa-sm-2
import { mapState } from 'vuex' import { mapState } from 'vuex'
import EventAdmin from './eventAdmin' import EventAdmin from './eventAdmin'
import EmbedEvent from './embedEvent' import EmbedEvent from './embedEvent'
import get from 'lodash/get'
import moment from 'dayjs' import moment from 'dayjs'
import clipboard from '../../assets/clipboard'
const htmlToText = require('html-to-text') const htmlToText = require('html-to-text')
export default { export default {
name: 'Event', name: 'Event',
mixins: [clipboard],
components: { EventAdmin, EmbedEvent }, components: { EventAdmin, EmbedEvent },
async asyncData ({ $axios, params, error, store }) { async asyncData ({ $axios, params, error, store }) {
try { try {
const event = await $axios.$get(`/event/${params.id}`) const event = await $axios.$get(`/event/${params.slug}`)
return { event, id: Number(params.id) } return { event }
} catch (e) { } catch (e) {
error({ statusCode: 404, message: 'Event not found' }) error({ statusCode: 404, message: 'Event not found' })
} }
}, },
data () { data () {
return { return {
currentAttachment: 0,
event: {}, event: {},
showEmbed: false, showEmbed: false,
showResources: false, showResources: false,
@ -153,8 +173,8 @@ export default {
const place_feed = { const place_feed = {
rel: 'alternate', rel: 'alternate',
type: 'application/rss+xml', type: 'application/rss+xml',
title: `${this.settings.title} events @${this.event.place.name}`, title: `${this.settings.title} events @${this.event.place && this.event.place.name}`,
href: this.settings.baseurl + `/feed/rss?places=${this.event.place.id}` href: this.settings.baseurl + `/feed/rss?places=${this.event.place && this.event.place.id}`
} }
return { return {
@ -180,7 +200,7 @@ export default {
{ property: 'og:type', content: 'event' }, { property: 'og:type', content: 'event' },
{ {
property: 'og:image', property: 'og:image',
content: this.thumbImgPath content: this.$options.filters.mediaURL(this.event)
}, },
{ property: 'og:site_name', content: this.settings.title }, { property: 'og:site_name', content: this.settings.title },
{ {
@ -196,7 +216,7 @@ export default {
{ property: 'twitter:title', content: this.event.title }, { property: 'twitter:title', content: this.event.title },
{ {
property: 'twitter:image', property: 'twitter:image',
content: this.thumbImgPath content: this.$options.filters.mediaURL(this.event, 'thumb')
}, },
{ {
property: 'twitter:description', property: 'twitter:description',
@ -204,7 +224,7 @@ export default {
} }
], ],
link: [ link: [
{ rel: 'image_src', href: this.thumbImgPath }, { rel: 'image_src', href: this.$options.filters.mediaURL(this.event, 'thumb') },
{ {
rel: 'alternate', rel: 'alternate',
type: 'application/rss+xml', type: 'application/rss+xml',
@ -218,14 +238,14 @@ export default {
}, },
computed: { computed: {
...mapState(['settings']), ...mapState(['settings']),
hasMedia () {
return this.event.media && this.event.media.length
},
plainDescription () { plainDescription () {
return htmlToText.fromString(this.event.description.replace('\n', '').slice(0, 1000)) return htmlToText.fromString(this.event.description.replace('\n', '').slice(0, 1000))
}, },
imgPath () { currentAttachmentLabel () {
return '/media/' + this.event.image_path return get(this.selectedResource, `data.attachment[${this.currentAttachment}].name`, '')
},
thumbImgPath () {
return this.settings.baseurl + '/media/thumb/' + this.event.image_path
}, },
is_mine () { is_mine () {
if (!this.$auth.user) { if (!this.$auth.user) {
@ -243,6 +263,14 @@ export default {
window.removeEventListener('keydown', this.keyDown) window.removeEventListener('keydown', this.keyDown)
}, },
methods: { methods: {
isImg (attachment) {
const type = attachment.mediaType.split('/')[0]
return type === 'image'
},
isAudio (attachment) {
const type = attachment.mediaType.split('/')[0]
return type === 'audio'
},
keyDown (ev) { keyDown (ev) {
if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) { return } if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) { return }
if (ev.key === 'ArrowRight' && this.event.next) { if (ev.key === 'ArrowRight' && this.event.next) {
@ -255,7 +283,7 @@ export default {
showResource (resource) { showResource (resource) {
this.showResources = true this.showResources = true
this.selectedResource = resource this.selectedResource = resource
document.getElementById('resourceDialog').focus() // document.getElementById('resourceDialog').focus()
}, },
async hideResource (resource, hidden) { async hideResource (resource, hidden) {
await this.$axios.$put(`/resources/${resource.id}`, { hidden }) await this.$axios.$put(`/resources/${resource.id}`, { hidden })
@ -298,16 +326,9 @@ export default {
} }
} }
</script> </script>
<style lang='less'> <style scoped>
.title {
margin-bottom: 25px;
color: yellow;
font-weight: 300 !important;
}
.main_image { .main_image {
// width: 100%;
margin: 0 auto; margin: 0 auto;
// max-height: 120vh;
border-radius: 5px; border-radius: 5px;
transition: max-height 0.2s; transition: max-height 0.2s;
} }

View file

@ -2,41 +2,32 @@
v-card v-card
v-card-title(v-text="$t('common.embed_title')") v-card-title(v-text="$t('common.embed_title')")
v-card-text v-card-text
v-row v-alert.mb-3.mt-1(type='info' show-icon) {{$t('common.embed_help')}}
v-col.col-12 v-alert.pa-5.my-4.blue-grey.darken-4.text-body-1.lime--text.text--lighten-3 <pre>{{code}}</pre>
v-alert.mb-1.mt-1(type='info' show-icon) {{$t('common.embed_help')}} v-btn.float-end(text color='primary' @click='clipboard(code)') {{$t("common.copy")}}
v-text-field(v-model='code')
v-btn(slot='prepend' text color='primary'
v-clipboard:copy='code'
v-clipboard:success='copyLink') {{$t("common.copy")}}
v-icon.ml-1 mdi-content-copy v-icon.ml-1 mdi-content-copy
p.mx-auto
v-col.mt-2(v-html='code') .mx-auto
gancio-event(:id='event.id' :baseurl='settings.baseurl')
v-card-actions v-card-actions
v-spacer v-spacer
v-btn(color='warning' @click="$emit('close')") {{$t("common.cancel")}} v-btn(text color='warning' @click="$emit('close')") {{$t("common.cancel")}}
v-btn(v-clipboard:copy='code' v-clipboard:success='copyLink' color="primary") {{$t("common.copy")}} v-btn(text @click='clipboard(code)' color="primary") {{$t("common.copy")}}
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import clipboard from '../../assets/clipboard'
export default { export default {
name: 'EmbedEvent', name: 'EmbedEvent',
mixins: [clipboard],
props: { props: {
event: { type: Object, default: () => ({}) } event: { type: Object, default: () => ({}) }
}, },
computed: { computed: {
...mapState(['settings']), ...mapState(['settings']),
code () { code () {
const style = "style='border: 0; width: 100%; height: 215px;'" return `<script src='${this.settings.baseurl}\/gancio-events.es.js'><\/script>\n<gancio-event baseurl='${this.settings.baseurl}' id=${this.event.id}></gancio-event>\n\n`
const src = `${this.settings.baseurl}/embed/${this.event.slug || this.event.id}`
const code = `<iframe ${style} src="${src}"></iframe>`
return code
}
},
methods: {
copyLink () {
this.$root.$message('common.copied', { color: 'success' })
} }
} }
} }

View file

@ -10,7 +10,7 @@
v-col v-col
Search( Search(
:filters='filters' :filters='filters'
@update='updateFilters') @update='f => filters = f')
v-tabs(v-model='type') v-tabs(v-model='type')
//- TOFIX //- TOFIX
@ -30,9 +30,7 @@
v-card-text v-card-text
p(v-html='$t(`export.feed_description`)') p(v-html='$t(`export.feed_description`)')
v-text-field(v-model='link' readonly) v-text-field(v-model='link' readonly)
v-btn(slot='prepend' text color='primary' v-btn(slot='prepend' text color='primary' @click='clipboard(link)') {{$t("common.copy")}}
v-clipboard:copy='link'
v-clipboard:success='copyLink.bind(this, "feed")') {{$t("common.copy")}}
v-icon.ml-1 mdi-content-copy v-icon.ml-1 mdi-content-copy
v-tab ics/ical v-tab ics/ical
@ -41,8 +39,7 @@
v-card-text v-card-text
p(v-html='$t(`export.ical_description`)') p(v-html='$t(`export.ical_description`)')
v-text-field(v-model='link') v-text-field(v-model='link')
v-btn(slot='prepend' text color='primary' v-btn(slot='prepend' text color='primary' @click='clipboard(link)') {{$t("common.copy")}}
v-clipboard:copy='link' v-clipboard:success='copyLink.bind(this, "ical")') {{$t("common.copy")}}
v-icon.ml-1 mdi-content-copy v-icon.ml-1 mdi-content-copy
v-tab List v-tab List
@ -54,16 +51,17 @@
v-row v-row
v-col.mr-2(:span='11') v-col.mr-2(:span='11')
v-text-field(v-model='list.title' :label='$t("common.title")') v-text-field(v-model='list.title' :label='$t("common.title")')
v-text-field(v-model='list.maxEvents' type='number' :label='$t("common.max_events")') v-text-field(v-model='list.maxEvents' type='number' min='1' :label='$t("common.max_events")')
v-col.float-right(:span='12') v-col.float-right(:span='12')
List( span {{filters.places.join(',')}}
gancio-events(:baseurl='settings.baseurl'
:maxlength='list.maxEvents && Number(list.maxEvents)'
:title='list.title' :title='list.title'
:maxEvents='list.maxEvents' :places='filters.places.join(",")'
:events='events') :tags='filters.tags.join(",")')
v-text-field.mb-1(type='textarea' v-model='listScript' readonly ) v-alert.pa-5.my-4.blue-grey.darken-4.text-body-1.lime--text.text--lighten-3 <pre>{{code}}</pre>
v-btn(slot='prepend' text v-btn.float-end(text color='primary' @click='clipboard(code)') {{$t("common.copy")}}
color='primary' v-clipboard:copy='listScript' v-clipboard:success='copyLink.bind(this,"list")') {{$t('common.copy')}} v-icon.ml-1 mdi-content-copy
v-icon.ml-1 mdi-content-copy
v-tab(v-if='settings.enable_federation') {{$t('common.fediverse')}} v-tab(v-if='settings.enable_federation') {{$t('common.fediverse')}}
v-tab-item(v-if='settings.enable_federation') v-tab-item(v-if='settings.enable_federation')
@ -84,10 +82,12 @@ import { mapState } from 'vuex'
import List from '@/components/List' import List from '@/components/List'
import FollowMe from '../components/FollowMe' import FollowMe from '../components/FollowMe'
import Search from '@/components/Search' import Search from '@/components/Search'
import clipboard from '../assets/clipboard'
export default { export default {
name: 'Exports', name: 'Exports',
components: { List, FollowMe, Search }, components: { List, FollowMe, Search },
mixins: [clipboard],
async asyncData ({ $axios, params, store, $api }) { async asyncData ({ $axios, params, store, $api }) {
const events = await $api.getEvents({ const events = await $api.getEvents({
start: dayjs().unix(), start: dayjs().unix(),
@ -99,7 +99,7 @@ export default {
return { return {
type: 'rss', type: 'rss',
notification: { email: '' }, notification: { email: '' },
list: { title: 'Gancio', maxEvents: 3 }, list: { title: 'Gancio', maxEvents: null },
filters: { tags: [], places: [], show_recurrent: false }, filters: { tags: [], places: [], show_recurrent: false },
events: [] events: []
} }
@ -111,28 +111,32 @@ export default {
}, },
computed: { computed: {
...mapState(['settings']), ...mapState(['settings']),
domain () { code () {
const URL = url.parse(this.settings.baseurl) const params = [`baseurl="${this.settings.baseurl}"`]
return URL.hostname
},
listScript () {
const params = []
if (this.list.title) { if (this.list.title) {
params.push(`title=${this.list.title}`) params.push(`title="${this.list.title}"`)
} }
if (this.filters.places.length) { if (this.filters.places.length) {
params.push(`places=${this.filters.places.map(p => p.id)}`) params.push(`places="${this.filters.places.join(',')}"`)
} }
if (this.filters.tags.length) { if (this.filters.tags.length) {
params.push(`tags=${this.filters.tags.join(',')}`) params.push(`tags="${this.filters.tags.join(',')}"`)
} }
if (this.filters.show_recurrent) { if (this.filters.show_recurrent) {
params.push('show_recurrent=true') params.push('show_recurrent')
} }
return `<iframe style='border: 0px; width: 100%;' src="${this.settings.baseurl}/embed/list?${params.join('&')}"></iframe>`
if (this.list.maxEvents) {
params.push('maxlength=' + this.list.maxEvents)
}
return `<script src="${this.settings.baseurl}\/gancio-events.es.js'><\/script>\n<gancio-events ${params.join(' ')}></gancio-events>\n\n`
}, },
link () { link () {
const typeMap = ['rss', 'ics', 'list'] const typeMap = ['rss', 'ics', 'list']
@ -157,22 +161,6 @@ export default {
} }
}, },
methods: { methods: {
async updateFilters (filters) {
this.filters = filters
this.events = await this.$api.getEvents({
start: dayjs().unix(),
places: this.filters.places,
tags: this.filters.tags,
show_recurrent: !!this.filters.show_recurrent
})
},
copyLink (type) {
if (type === 'feed') {
this.$root.$message('common.feed_url_copied')
} else {
this.$root.$message('common.copied')
}
},
async add_notification () { async add_notification () {
// validate() // validate()
// if (!this.notification.email) { // if (!this.notification.email) {
@ -184,7 +172,7 @@ export default {
// Message({ message: this.$t('email_notification_activated'), showClose: true, type: 'success' }) // Message({ message: this.$t('email_notification_activated'), showClose: true, type: 'success' })
}, },
imgPath (event) { imgPath (event) {
return event.image_path && event.image_path return event.media && event.media[0].url
} }
} }
} }

View file

@ -10,7 +10,7 @@
.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-4.pa-sm-3 .col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-4.pa-sm-3
//- this is needed as v-calendar does not support SSR //- this is needed as v-calendar does not support SSR
//- https://github.com/nathanreyes/v-calendar/issues/336 //- https://github.com/nathanreyes/v-calendar/issues/336
client-only client-only(placeholder='Calendar unavailable without js')
Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='filteredEvents') Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='filteredEvents')
.col.pt-0.pt-md-2 .col.pt-0.pt-md-2
@ -36,6 +36,7 @@ import Calendar from '@/components/Calendar'
export default { export default {
name: 'Index', name: 'Index',
components: { Event, Search, Announcement, Calendar }, components: { Event, Search, Announcement, Calendar },
middleware: 'setup',
async asyncData ({ params, $api, store }) { async asyncData ({ params, $api, store }) {
const events = await $api.getEvents({ const events = await $api.getEvents({
start: dayjs().startOf('month').unix(), start: dayjs().startOf('month').unix(),
@ -65,7 +66,7 @@ export default {
{ hid: 'og-description', name: 'og:description', content: this.settings.description }, { hid: 'og-description', name: 'og:description', content: this.settings.description },
{ hid: 'og-title', property: 'og:title', content: this.settings.title }, { hid: 'og-title', property: 'og:title', content: this.settings.title },
{ hid: 'og-url', property: 'og:url', content: this.settings.baseurl }, { hid: 'og-url', property: 'og:url', content: this.settings.baseurl },
{ property: 'og:image', content: this.settings.baseurl + '/favicon.ico' } { property: 'og:image', content: this.settings.baseurl + '/logo.png' }
], ],
link: [ link: [
{ rel: 'alternate', type: 'application/rss+xml', title: this.settings.title, href: this.settings.baseurl + '/feed/rss' } { rel: 'alternate', type: 'application/rss+xml', title: this.settings.title, href: this.settings.baseurl + '/feed/rss' }

View file

@ -1,33 +1,34 @@
<template lang='pug'> <template lang='pug'>
v-container
v-row.mt-5(align='center' justify='center') v-row.mt-5(align='center' justify='center')
v-col(cols='12' md="6" lg="5" xl="4") v-col(cols='12' md="6" lg="5" xl="4")
v-card v-card
v-card-title {{settings.title}} - {{$t('common.recover_password')}} v-card-title {{$t('common.recover_password')}}
v-card-text template(v-if='user')
div(v-if='valid') v-card-subtitle {{user.email}}
v-card-text
v-text-field(type='password' v-text-field(type='password'
:rules="$validators.password" :rules="$validators.password"
autofocus :placeholder='$t("common.new_password")' autofocus :placeholder='$t("common.new_password")'
v-model='new_password') v-model='new_password')
div(v-else) {{$t('recover.not_valid_code')}} div(v-else) {{$t('recover.not_valid_code')}}
v-card-actions v-card-actions
v-spacer v-spacer
v-btn(v-if='valid' color='primary' @click='change_password') {{$t('common.send')}} v-btn(v-if='user' text color='primary' @click='change_password') {{$t('common.send')}}
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
export default { export default {
name: 'Recover', name: 'Recover',
layout: 'modal',
async asyncData ({ params, $axios }) { async asyncData ({ params, $axios }) {
const code = params.code const code = params.code
try { try {
const valid = await $axios.$post('/user/check_recover_code', { recover_code: code }) const user = await $axios.$post('/user/check_recover_code', { recover_code: code })
return { valid, code } return { user, code }
} catch (e) { } catch (e) {
return { valid: false } return { user: false }
} }
}, },
data () { data () {
@ -50,7 +51,7 @@ export default {
} }
} }
</script> </script>
<style lang='less'> <style>
h4 img { h4 img {
max-height: 40px; max-height: 40px;
border-radius: 20px; border-radius: 20px;

View file

@ -8,7 +8,6 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import url from 'url'
export default { export default {
name: 'Settings', name: 'Settings',
@ -19,12 +18,7 @@ export default {
user: { } user: { }
} }
}, },
computed: { computed: mapState(['settings']),
...mapState(['settings']),
baseurl () {
return url.parse(this.settings.baseurl).host
}
},
methods: { methods: {
// async change_password () { // async change_password () {
// if (!this.password) { return } // if (!this.password) { return }

38
pages/setup/Completed.vue Normal file
View file

@ -0,0 +1,38 @@
<template lang="pug">
v-container
v-card-title.d-block.text-h5.text-center(v-text="$t('setup.completed')")
v-card-text(v-html="$t('setup.completed_description', user)")
v-card-actions
v-btn(text @click='next' color='primary' :loading='loading' :disabled='loading') {{$t('setup.start')}}
v-icon mdi-arrow-right
</template>
<script>
export default {
data () {
return {
loading: false,
user: {
email: 'admin',
password: ''
}
}
},
methods: {
next () {
window.location='/admin'
},
async start (user) {
this.user = { ...user }
this.loading = true
try {
await this.$axios.$get('/ping')
this.loading = false
} catch (e) {
setTimeout(() => this.start(user), 1000)
}
}
}
}
</script>

47
pages/setup/DbStep.vue Normal file
View file

@ -0,0 +1,47 @@
<template lang="pug">
v-container
v-card-title.text-h5 Database
v-card-text
v-form
v-btn-toggle(text color='primary' v-model='db.dialect')
v-btn(value='sqlite' text) sqlite
v-btn(value='postgres' text) postgres
template(v-if='db.dialect === "sqlite"')
v-text-field(v-model='db.storage' label='Path')
template(v-if='db.dialect === "postgres"')
v-text-field(v-model='db.hostname' label='Hostname' :rules="[$validators.required('hostname')]")
v-text-field(v-model='db.database' label='Database' :rules="[$validators.required('database')]")
v-text-field(v-model='db.username' label='Username' :rules="[$validators.required('username')]")
v-text-field(type='password' v-model='db.password' label='Password' :rules="[$validators.required('password')]")
v-card-actions
v-btn(text @click='checkDb' color='primary' :loading='loading' :disabled='loading') {{$t('common.next')}}
v-icon mdi-arrow-right
</template>
<script>
export default {
data () {
return {
db: {
storage: './gancio.sqlite',
hostname: 'localhost',
database: 'gancio'
},
loading: false
}
},
methods: {
async checkDb () {
this.loading = true
try {
await this.$axios.$post('/setup/db', { db: this.db })
this.$root.$message('DB Connection OK!', { color: 'success' })
this.$emit('complete', this.db)
} catch (e) {
this.$root.$message(e.response.data, { color: 'error' })
}
this.loading = false
}
}
}
</script>

64
pages/setup/index.vue Normal file
View file

@ -0,0 +1,64 @@
<template lang="pug">
v-container.pa-6
h2.mb-2.text-center Gancio Setup
v-stepper.grey.lighten-5(v-model='step')
v-stepper-header
v-stepper-step(:complete='step > 1' step='1') Database
v-divider
v-stepper-step(:complete='step > 2' step='2') Configuration
v-divider
v-stepper-step(:complete='step > 3' step='3') Finish
v-stepper-items
v-stepper-content(step='1')
DbStep(@complete='dbCompleted')
v-stepper-content(step='2')
Settings(setup, @complete='configCompleted')
v-stepper-content(step='3')
Completed(ref='completed')
</template>
<script>
import DbStep from './DbStep'
import Settings from '../../components/admin/Settings'
import Completed from './Completed'
export default {
components: { DbStep, Settings, Completed },
middleware: 'setup',
layout: 'clean',
head: {
title: 'Setup',
},
auth: false,
data () {
return {
config: {
db: {
dialect: ''
}
},
step: 1
}
},
methods: {
dbCompleted (db) {
this.step = this.step + 1
},
async configCompleted () {
try {
const user = await this.$axios.$post('/setup/restart')
this.step = this.step + 1
this.$refs.completed.start(user)
} catch (e) {
this.$root.$message(e.response ? e.response.data : e, { color: 'error' })
}
}
}
}
</script>

View file

@ -1,17 +1,21 @@
<template lang="pug"> <template lang="pug">
v-container
v-row.mt-5(align='center' justify='center') v-row.mt-5(align='center' justify='center')
v-col(cols='12' md="6" lg="5" xl="4") v-col(cols='12' md="6" lg="5" xl="4")
v-card v-card
v-card-title <nuxt-link to='/'><img src='/favicon.ico'/></nuxt-link> {{$t('common.set_password')}} v-card-title {{$t('common.set_password')}}
template(v-if='valid') template(v-if='user')
v-card-text(v-if='valid') v-card-subtitle {{user.email}}
v-form(v-if='valid') v-card-text
v-text-field(type='password' v-model='new_password' :label="$t('common.new_password')") v-form
v-text-field(type='password' v-model='new_password' :label="$t('common.new_password')" :rules='$validators.password' autofocus)
v-card-actions v-card-actions
v-btn(color="success" :disabled='!new_password' @click='change_password') {{$t('common.send')}} v-spacer
v-btn(text color="primary" :disabled='!new_password' @click='change_password') {{$t('common.send')}}
v-card-text(v-else) {{$t('recover.not_valid_code')}} v-card-text(v-else)
v-alert.ma-5(type='error') {{$t('recover.not_valid_code')}}
</template> </template>
<script> <script>
@ -21,10 +25,10 @@ export default {
async asyncData ({ params, $axios }) { async asyncData ({ params, $axios }) {
const code = params.code const code = params.code
try { try {
const valid = await $axios.$post('/user/check_recover_code', { recover_code: code }) const user = await $axios.$post('/user/check_recover_code', { recover_code: code })
return { valid, code } return { user, code }
} catch (e) { } catch (e) {
return { valid: false } return { user: false }
} }
}, },
data () { data () {

View file

@ -1,44 +0,0 @@
<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.set_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>
export default {
name: 'Recover',
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 }
}
},
data () {
return { new_password: '' }
},
methods: {
async change_password () {
try {
await this.$axios.$post('/user/recover_password', { recover_code: this.code, password: this.new_password })
this.$root.$message('common.password_updated', { color: 'succes' })
this.$router.replace('/login')
} catch (e) {
this.$root.$message(e, { color: 'warning' })
}
}
}
}
</script>

View file

@ -1,5 +1,5 @@
export default ({ $axios, store }, inject) => { export default ({ $axios }, inject) => {
const api = { const api = {
/** /**

View file

@ -33,6 +33,18 @@ export default ({ app, store }) => {
// shown in mobile homepage // shown in mobile homepage
Vue.filter('day', value => dayjs.unix(value).locale(store.state.locale).format('dddd, D MMM')) Vue.filter('day', value => dayjs.unix(value).locale(store.state.locale).format('dddd, D MMM'))
Vue.filter('mediaURL', (event, type) => {
if (event.media && event.media.length) {
if (type === 'alt') {
return event.media[0].name
} else {
return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + event.media[0].url
}
} else if (type !== 'alt') {
return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + 'logo.svg'
}
return ''
})
Vue.filter('from', timestamp => dayjs.unix(timestamp).fromNow()) Vue.filter('from', timestamp => dayjs.unix(timestamp).fromNow())

View file

@ -0,0 +1,28 @@
const plugin = {
gancio: null,
load (gancio) {
console.error('Plugin GancioPluginExample loaded!')
plugin.gancio = gancio
},
onEventCreate (event) {
const eventLink = `${plugin.gancio.settings.baseurl}/event/${event.slug}`
if (!event.is_visible) {
console.error(`Unconfirmed event created: ${event.title} / ${eventLink}`)
} else {
console.error(`Event created: ${event.title} / ${eventLink}`)
}
},
onEventUpdate (event) {
console.error(`Event "${event.title}" updated`)
},
onEventDelete (event) {
console.error(`Event "${event.title}" deleted`)
}
}
module.exports = plugin

View file

@ -4,18 +4,20 @@ import merge from 'lodash/merge'
Vue.use(VueI18n) Vue.use(VueI18n)
export default ({ app, store, req }) => { export default async ({ app, store, req }) => {
const messages = {}
if (process.server) { if (process.server) {
store.commit('setLocale', req.settings.locale) store.commit('setLocale', req.acceptedLocale)
if (req.settings.user_locale) { store.commit('setUserLocale', req.settings.user_locale) } if (req.user_locale) {
store.commit('setUserLocale', req.user_locale)
}
} }
const messages = {} messages[store.state.locale] = await import(/* webpackChunkName: "lang-[request]" */`../locales/${store.state.locale}.json`)
messages[store.state.locale] = require(`../locales/${store.state.locale}.json`)
// always include en fallback locale // always include en fallback locale
if (store.state.locale !== 'en') { if (store.state.locale !== 'en') {
messages.en = require('../locales/en.json') messages.en = await import('../locales/en.json')
} }
if (store.state.user_locale) { if (store.state.user_locale) {

View file

@ -8,7 +8,7 @@ export default ({ app }, inject) => {
}, },
email: [ email: [
v => !!v || $t('validators.required', { fieldName: $t('common.email') }), v => !!v || $t('validators.required', { fieldName: $t('common.email') }),
v => (v && !!linkify.test(v, 'email')) || $t('validators.email') v => (v && (v === 'admin' || !!linkify.test(v, 'email')) || $t('validators.email'))
], ],
password: [ password: [
v => !!v || $t('validators.required', { fieldName: $t('common.password') }) v => !!v || $t('validators.required', { fieldName: $t('common.password') })

View file

@ -1,6 +0,0 @@
import Vue from 'vue'
import VueClipboard from 'vue-clipboard2'
export default () => {
Vue.use(VueClipboard)
}

34
plugins/vuetify.js Normal file
View file

@ -0,0 +1,34 @@
import Vue from 'vue'
import Vuetify from 'vuetify'
// import it from 'vuetify/lib/locale/it.js'
// import en from 'vuetify/lib/locale/en.js'
// import es from 'vuetify/lib/locale/es'
// import no from 'vuetify/lib/locale/no'
// import fr from 'vuetify/lib/locale/fr'
// import ca from 'vuetify/lib/locale/ca'
export default ({ app }) => {
Vue.use(Vuetify)
app.vuetify = new Vuetify({
// lang: {
// locales: { en, it }, //, es, fr, no, ca },
// current: 'en'
// },
icons: {
iconfont: 'mdi'
},
theme: {
dark: true,
themes: {
dark: {
primary: '#FF6E40'
},
light: {
primary: '#FF4500'
}
}
}
})
}

View file

@ -17,7 +17,7 @@ const announceController = {
announcement: req.body.announcement, announcement: req.body.announcement,
visible: true visible: true
} }
log.info('Create announcement: "%s" ', req.body.title) log.info('Create announcement: ' + req.body.title)
const announce = await Announcement.create(announcementDetail) const announce = await Announcement.create(announcementDetail)
res.json(announce) res.json(announce)
}, },
@ -34,20 +34,20 @@ const announceController = {
announce = await announce.update(announceDetails) announce = await announce.update(announceDetails)
res.json(announce) res.json(announce)
} catch (e) { } catch (e) {
log.error('Toggle announcement failed: %s ', e) log.error('Toggle announcement failed', e)
res.sendStatus(404) res.sendStatus(404)
} }
}, },
async remove (req, res) { async remove (req, res) {
log.info('Remove announcement "%d"', req.params.announce_id) log.info('Remove announcement', req.params.announce_id)
const announce_id = req.params.announce_id const announce_id = req.params.announce_id
try { try {
const announce = await Announcement.findByPk(announce_id) const announce = await Announcement.findByPk(announce_id)
await announce.destroy() await announce.destroy()
res.sendStatus(200) res.sendStatus(200)
} catch (e) { } catch (e) {
log.error('Remove announcement failed: "%s" ', e) log.error('Remove announcement failed:', e)
res.sendStatus(404) res.sendStatus(404)
} }
} }

View file

@ -1,10 +1,10 @@
const crypto = require('crypto') const crypto = require('crypto')
const path = require('path') const path = require('path')
const config = require('config') const config = require('../../config')
const fs = require('fs') const fs = require('fs')
const { Op } = require('sequelize') const { Op } = require('sequelize')
const intersection = require('lodash/intersection') const intersection = require('lodash/intersection')
const linkifyHtml = require('linkifyjs/html') const linkifyHtml = require('linkify-html')
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const helpers = require('../../helpers') const helpers = require('../../helpers')
@ -85,11 +85,26 @@ const eventController = {
res.json(place) res.json(place)
}, },
async _get(slug) {
// retrocompatibility, old events URL does not use slug, use id as fallback
const id = Number(slug) || -1
return Event.findOne({
where: {
[Op.or]: {
slug,
id
}
}
})
},
async get (req, res) { async get (req, res) {
const format = req.params.format || 'json' const format = req.params.format || 'json'
const is_admin = req.user && req.user.is_admin const is_admin = req.user && req.user.is_admin
const slug = req.params.event_id const slug = req.params.event_slug
const id = Number(req.params.event_id) || -1
// retrocompatibility, old events URL does not use slug, use id as fallback
const id = Number(slug) || -1
let event let event
try { try {
@ -118,7 +133,7 @@ const eventController = {
order: [[Resource, 'id', 'DESC']] order: [[Resource, 'id', 'DESC']]
}) })
} catch (e) { } catch (e) {
log.error(e) log.error('[EVENT]', e)
return res.sendStatus(400) return res.sendStatus(400)
} }
@ -194,7 +209,7 @@ const eventController = {
const notifier = require('../../notifier') const notifier = require('../../notifier')
notifier.notifyEvent('Create', event.id) notifier.notifyEvent('Create', event.id)
} catch (e) { } catch (e) {
log.error(e) log.error('[EVENT]', e)
res.sendStatus(404) res.sendStatus(404)
} }
}, },
@ -264,7 +279,7 @@ const eventController = {
async add (req, res) { async add (req, res) {
// req.err comes from multer streaming error // req.err comes from multer streaming error
if (req.err) { if (req.err) {
log.info(req.err) log.warn(req.err)
return res.status(400).json(req.err.toString()) return res.status(400).json(req.err.toString())
} }
@ -272,6 +287,11 @@ const eventController = {
const body = req.body const body = req.body
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
if (!body.place_name) {
log.warn('Place is required')
return res.status(400).send('Place is required')
}
const eventDetails = { const eventDetails = {
title: body.title, title: body.title,
// remove html tags // remove html tags
@ -284,10 +304,23 @@ const eventController = {
is_visible: !!req.user is_visible: !!req.user
} }
if (req.file) { if (req.file || body.image_url) {
eventDetails.image_path = req.file.filename let url
} else if (body.image_url) { if (req.file) {
eventDetails.image_path = await helpers.getImageFromURL(body.image_url) url = req.file.filename
} else {
url = await helpers.getImageFromURL(body.image_url)
}
let focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
focalpoint = [parseFloat(focalpoint[0]).toFixed(2), parseFloat(focalpoint[1]).toFixed(2)]
eventDetails.media = [{
url,
name: body.image_name || body.title || '',
focalpoint: [parseFloat(focalpoint[0]), parseFloat(focalpoint[1])]
}]
} else {
eventDetails.media = []
} }
const event = await Event.create(eventDetails) const event = await Event.create(eventDetails)
@ -324,12 +357,12 @@ const eventController = {
if (event.recurrent) { if (event.recurrent) {
eventController._createRecurrent() eventController._createRecurrent()
} else { } else {
// send notifications (mastodon / email) // send notifications
const notifier = require('../../notifier') const notifier = require('../../notifier')
notifier.notifyEvent('Create', event.id) notifier.notifyEvent('Create', event.id)
} }
} catch (e) { } catch (e) {
log.error(e) log.error('[EVENT ADD]', e)
res.sendStatus(400) res.sendStatus(400)
} }
}, },
@ -338,28 +371,29 @@ const eventController = {
if (req.err) { if (req.err) {
return res.status(400).json(req.err.toString()) return res.status(400).json(req.err.toString())
} }
const body = req.body
const event = await Event.findByPk(body.id)
if (!event) { return res.sendStatus(404) }
if (!req.user.is_admin && event.userId !== req.user.id) {
return res.sendStatus(403)
}
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null try {
const eventDetails = { const body = req.body
title: body.title, const event = await Event.findByPk(body.id)
// remove html tags if (!event) { return res.sendStatus(404) }
description: helpers.sanitizeHTML(linkifyHtml(body.description, { target: '_blank' })), if (!req.user.is_admin && event.userId !== req.user.id) {
multidate: body.multidate, return res.sendStatus(403)
start_datetime: body.start_datetime, }
end_datetime: body.end_datetime,
recurrent
}
if (req.file) { const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
if (event.image_path && !event.recurrent) { const eventDetails = {
const old_path = path.resolve(config.upload_path, event.image_path) title: body.title,
const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.image_path) // remove html tags
description: helpers.sanitizeHTML(linkifyHtml(body.description, { target: '_blank' })),
multidate: body.multidate,
start_datetime: body.start_datetime,
end_datetime: body.end_datetime,
recurrent
}
if ((req.file || /^https?:\/\//.test(body.image_url)) && !event.recurrent && event.media && event.media.length) {
const old_path = path.resolve(config.upload_path, event.media[0].url)
const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.media[0].url)
try { try {
fs.unlinkSync(old_path) fs.unlinkSync(old_path)
fs.unlinkSync(old_thumb_path) fs.unlinkSync(old_thumb_path)
@ -367,34 +401,55 @@ const eventController = {
log.info(e.toString()) log.info(e.toString())
} }
} }
eventDetails.image_path = req.file.filename let url
} else if (body.image_url) { if (req.file) {
eventDetails.image_path = await helpers.getImageFromURL(body.image_url) url = req.file.filename
} } else if (body.image_url) {
if (/^https?:\/\//.test(body.image_url)) {
url = await helpers.getImageFromURL(body.image_url)
} else {
url = body.image_url
}
}
await event.update(eventDetails) if (url && !event.recurrent) {
const [place] = await Place.findOrCreate({ const focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
where: { name: body.place_name }, eventDetails.media = [{
defaults: { address: body.place_address } url,
}) name: body.image_name || '',
focalpoint: [parseFloat(focalpoint[0].slice(0, 6)), parseFloat(focalpoint[1].slice(0, 6))]
}]
} else {
eventDetails.media = []
}
await event.setPlace(place) await event.update(eventDetails)
await event.setTags([]) const [place] = await Place.findOrCreate({
if (body.tags) { where: { name: body.place_name },
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true }) defaults: { address: body.place_address }
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } }) })
await event.addTags(tags)
}
const newEvent = await Event.findByPk(event.id, { include: [Tag, Place] })
res.json(newEvent)
// create recurrent instances of event if needed await event.setPlace(place)
// without waiting for the task manager await event.setTags([])
if (event.recurrent) { if (body.tags) {
eventController._createRecurrent() await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
} else { const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
const notifier = require('../../notifier') await event.addTags(tags)
notifier.notifyEvent('Update', event.id) }
const newEvent = await Event.findByPk(event.id, { include: [Tag, Place] })
res.json(newEvent)
// create recurrent instances of event if needed
// without waiting for the task manager
if (event.recurrent) {
eventController._createRecurrent()
} else {
const notifier = require('../../notifier')
notifier.notifyEvent('Update', event.id)
}
} catch (e) {
log.error('[EVENT UPDATE]', e)
res.sendStatus(400)
} }
}, },
@ -402,9 +457,9 @@ const eventController = {
const event = await Event.findByPk(req.params.id) const event = await Event.findByPk(req.params.id)
// check if event is mine (or user is admin) // check if event is mine (or user is admin)
if (event && (req.user.is_admin || req.user.id === event.userId)) { if (event && (req.user.is_admin || req.user.id === event.userId)) {
if (event.image_path && !event.recurrent) { if (event.media && event.media.length && !event.recurrent) {
const old_path = path.join(config.upload_path, event.image_path) const old_path = path.join(config.upload_path, event.media[0].url)
const old_thumb_path = path.join(config.upload_path, 'thumb', event.image_path) const old_thumb_path = path.join(config.upload_path, 'thumb', event.media[0].url)
try { try {
fs.unlinkSync(old_thumb_path) fs.unlinkSync(old_thumb_path)
fs.unlinkSync(old_path) fs.unlinkSync(old_path)
@ -414,6 +469,12 @@ const eventController = {
} }
const notifier = require('../../notifier') const notifier = require('../../notifier')
await notifier.notifyEvent('Delete', event.id) await notifier.notifyEvent('Delete', event.id)
// unassociate child events
if (event.recurrent) {
await Event.update({ parentId: null }, { where: { parentId: event.id } })
}
log.debug('[EVENT REMOVED] ' + event.title)
await event.destroy() await event.destroy()
res.sendStatus(200) res.sendStatus(200)
} else { } else {
@ -421,7 +482,8 @@ const eventController = {
} }
}, },
async _select ({ start, end, tags, places, show_recurrent }) { async _select ({ start, end, tags, places, show_recurrent, max }) {
const where = { const where = {
// do not include parent recurrent event // do not include parent recurrent event
recurrent: null, recurrent: null,
@ -448,7 +510,7 @@ const eventController = {
let where_tags = {} let where_tags = {}
if (tags) { if (tags) {
where_tags = { where: { tag: tags.split(',') } } where_tags = { where: { [Op.or]: { tag: tags.split(',') } } }
} }
const events = await Event.findAll({ const events = await Event.findAll({
@ -456,20 +518,23 @@ const eventController = {
attributes: { attributes: {
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources'] exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources']
}, },
order: ['start_datetime', Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE "tagTag" = tag) DESC')], order: ['start_datetime'],
include: [ include: [
{ model: Resource, required: false, attributes: ['id'] }, { model: Resource, required: false, attributes: ['id'] },
{ {
model: Tag, model: Tag,
order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
attributes: ['tag'], attributes: ['tag'],
required: !!tags, required: !!tags,
...where_tags, ...where_tags,
through: { attributes: [] } through: { attributes: [] }
}, },
{ model: Place, required: true, attributes: ['id', 'name', 'address'] } { model: Place, required: true, attributes: ['id', 'name', 'address'] }
] ],
limit: max
}).catch(e => { }).catch(e => {
log.error(e) log.error('[EVENT]', e)
return []
}) })
return events.map(e => { return events.map(e => {
@ -483,20 +548,22 @@ const eventController = {
* Select events based on params * Select events based on params
*/ */
async select (req, res) { async select (req, res) {
const start = req.query.start const start = req.query.start || dayjs().unix()
const end = req.query.end const end = req.query.end
const tags = req.query.tags const tags = req.query.tags
const places = req.query.places const places = req.query.places
const max = req.query.max
const show_recurrent = settingsController.settings.allow_recurrent_event && const show_recurrent = settingsController.settings.allow_recurrent_event &&
(typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settingsController.settings.recurrent_event_visible) (typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settingsController.settings.recurrent_event_visible)
res.json(await eventController._select({ res.json(await eventController._select({
start, end, places, tags, show_recurrent start, end, places, tags, show_recurrent, max
})) }))
}, },
/** /**
* Ensure we have the next instance of a recurrent event * Ensure we have the next instance of a recurrent event
* TODO: create a future instance if the next one is skipped
*/ */
_createRecurrentOccurrence (e) { _createRecurrentOccurrence (e) {
log.debug(`Create recurrent event [${e.id}] ${e.title}"`) log.debug(`Create recurrent event [${e.id}] ${e.title}"`)
@ -504,15 +571,16 @@ const eventController = {
parentId: e.id, parentId: e.id,
title: e.title, title: e.title,
description: e.description, description: e.description,
image_path: e.image_path, media: e.media,
is_visible: true, is_visible: true,
userId: e.userId, userId: e.userId,
placeId: e.placeId placeId: e.placeId
} }
const recurrent = e.recurrent const recurrent = e.recurrent
let cursor = dayjs()
const start_date = dayjs.unix(e.start_datetime) const start_date = dayjs.unix(e.start_datetime)
const now = dayjs()
let cursor = start_date > now ? start_date : now
const duration = dayjs.unix(e.end_datetime).diff(start_date, 's') const duration = dayjs.unix(e.end_datetime).diff(start_date, 's')
const frequency = recurrent.frequency const frequency = recurrent.frequency
const type = recurrent.type const type = recurrent.type

View file

@ -12,6 +12,7 @@ const exportController = {
const type = req.params.type const type = req.params.type
const tags = req.query.tags const tags = req.query.tags
const places = req.query.places const places = req.query.places
const show_recurrent = !!req.query.show_recurrent
const where = {} const where = {}
const yesterday = moment().subtract('1', 'day').unix() const yesterday = moment().subtract('1', 'day').unix()
@ -25,9 +26,13 @@ const exportController = {
where.placeId = places.split(',') where.placeId = places.split(',')
} }
if (!show_recurrent) {
where.parentId = null
}
const events = await Event.findAll({ const events = await Event.findAll({
order: ['start_datetime'], order: ['start_datetime'],
attributes: { exclude: ['is_visible', 'recurrent', 'createdAt', 'updatedAt', 'likes', 'boost', 'slug', 'userId', 'placeId'] }, attributes: { exclude: ['is_visible', 'recurrent', 'createdAt', 'likes', 'boost', 'userId', 'placeId'] },
where: { where: {
is_visible: true, is_visible: true,
recurrent: { [Op.eq]: null }, recurrent: { [Op.eq]: null },
@ -62,8 +67,8 @@ const exportController = {
const eventsMap = events.map(e => { const eventsMap = events.map(e => {
const tmpStart = moment.unix(e.start_datetime) const tmpStart = moment.unix(e.start_datetime)
const tmpEnd = moment.unix(e.end_datetime) const tmpEnd = moment.unix(e.end_datetime)
const start = tmpStart.utc(true).format('YYYY-M-D-H-m').split('-') const start = tmpStart.utc(true).format('YYYY-M-D-H-m').split('-').map(Number)
const end = tmpEnd.utc(true).format('YYYY-M-D-H-m').split('-') const end = tmpEnd.utc(true).format('YYYY-M-D-H-m').split('-').map(Number)
return { return {
start, start,
// startOutputType: 'utc', // startOutputType: 'utc',
@ -77,8 +82,12 @@ const exportController = {
} }
}) })
res.type('text/calendar; charset=UTF-8') res.type('text/calendar; charset=UTF-8')
const ret = ics.createEvents(eventsMap) ics.createEvents(eventsMap, (err, value) => {
res.send(ret.value) if (err) {
return res.status(401).send(err)
}
return res.send(value)
})
} }
} }

View file

@ -42,7 +42,7 @@ const oauthController = {
delete client.id delete client.id
res.json(client) res.json(client)
} catch (e) { } catch (e) {
log.error(e) log.error('[OAUTH CLIENT]', e)
res.status(400).json(e) res.status(400).json(e)
} }
}, },

View file

@ -1,4 +1,7 @@
const Resource = require('../models/resource') const Resource = require('../models/resource')
const APUser = require('../models/ap_user')
const Event = require('../models/event')
const get = require('lodash/get')
const resourceController = { const resourceController = {
async hide (req, res) { async hide (req, res) {
@ -17,12 +20,28 @@ const resourceController = {
}, },
async getAll (req, res) { async getAll (req, res) {
const limit = req.body.limit || 100 const limit = req.body.limit || 1000
// const where = {} // const where = {}
// if (req.params.instanceId) { // if (req.params.instanceId) {
// where = // where =
// //
const resources = await Resource.findAll({ limit }) let resources = await Resource.findAll({ limit, include: [APUser, Event], order: [['createdAt', 'DESC']] })
resources = resources.map(r => ({
id: r.id,
hidden: r.hidden,
created: r.createdAt,
data: {
content: r.data.content
},
event: {
id: r.event.id,
title: r.event.title
},
ap_user: {
ap_id: get(r, 'ap_user.ap_id', ''),
preferredUsername: get(r, 'ap_user.object.preferredUsername', '')
}
}))
res.json(resources) res.json(resources)
} }
} }

View file

@ -1,22 +1,29 @@
const Setting = require('../models/setting')
const config = require('config')
const consola = require('consola')
const path = require('path') const path = require('path')
const URL = require('url')
const fs = require('fs') const fs = require('fs')
const pkg = require('../../../package.json')
const crypto = require('crypto') const crypto = require('crypto')
const util = require('util') const { promisify } = require('util')
const toIco = require('to-ico')
const generateKeyPair = util.promisify(crypto.generateKeyPair)
const readFile = util.promisify(fs.readFile)
const writeFile = util.promisify(fs.writeFile)
const sharp = require('sharp') const sharp = require('sharp')
const config = require('../../config')
const pkg = require('../../../package.json')
const generateKeyPair = promisify(crypto.generateKeyPair)
const log = require('../../log') const log = require('../../log')
const locales = require('../../../locales/index')
let defaultHostname
try {
defaultHostname = new URL.URL(config.baseurl).hostname
} catch (e) {}
const defaultSettings = { const defaultSettings = {
title: config.title || 'Gancio',
description: config.description || 'A shared agenda for local communities',
baseurl: config.baseurl || '',
hostname: defaultHostname,
instance_timezone: 'Europe/Rome', instance_timezone: 'Europe/Rome',
instance_locale: 'en', instance_locale: 'en',
instance_name: config.title.toLowerCase().replace(/ /g, ''), instance_name: 'gancio',
instance_place: '', instance_place: '',
allow_registration: true, allow_registration: true,
allow_anon_event: true, allow_anon_event: true,
@ -32,7 +39,9 @@ const defaultSettings = {
footerLinks: [ footerLinks: [
{ href: '/', label: 'home' }, { href: '/', label: 'home' },
{ href: '/about', label: 'about' } { href: '/about', label: 'about' }
] ],
admin_email: config.admin_email || '',
smtp: config.smtp || false
} }
/** /**
@ -45,54 +54,88 @@ const settingsController = {
secretSettings: {}, secretSettings: {},
async load () { async load () {
if (!settingsController.settings.initialized) { if (config.firstrun) {
// initialize instance settings from db
// note that this is done only once when the server starts
// and not for each request (it's a kind of cache)!
const settings = await Setting.findAll()
settingsController.settings.initialized = true
settingsController.settings = defaultSettings settingsController.settings = defaultSettings
settings.forEach(s => { return
if (s.is_secret) { }
settingsController.secretSettings[s.key] = s.value if (settingsController.settings.initialized) return
} else { settingsController.settings.initialized = true
settingsController.settings[s.key] = s.value // initialize instance settings from db
// note that this is done only once when the server starts
// and not for each request
const Setting = require('../models/setting')
const settings = await Setting.findAll()
settingsController.settings = defaultSettings
settings.forEach(s => {
if (s.is_secret) {
settingsController.secretSettings[s.key] = s.value
} else {
settingsController.settings[s.key] = s.value
}
})
// add pub/priv instance key if needed
if (!settingsController.settings.publicKey) {
log.info('Instance priv/pub key not found, generating....')
const { publicKey, privateKey } = await generateKeyPair('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
} }
}) })
// add pub/priv instance key if needed await settingsController.set('publicKey', publicKey)
if (!settingsController.settings.publicKey) { await settingsController.set('privateKey', privateKey, true)
log.info('Instance priv/pub key not found, generating....') }
const { publicKey, privateKey } = await generateKeyPair('rsa', {
modulusLength: 4096, // initialize user_locale
publicKeyEncoding: { if (config.user_locale && fs.existsSync(path.resolve(config.user_locale))) {
type: 'spki', const user_locales_files = fs.readdirSync(path.resolve(config.user_locale))
format: 'pem' user_locales_files.forEach( f => {
}, const locale = path.basename(f ,'.json')
privateKeyEncoding: { if (locales[locale]) {
type: 'pkcs8', log.info(`Adding custom locale ${locale}`)
format: 'pem' settingsController.user_locale[locale] = require(path.resolve(config.user_locale, f)).default
} else {
log.warning(`Unknown custom user locale: ${locale} [valid locales are ${locales}]`)
}
})
}
// load custom plugins
const plugins_path = path.resolve(process.env.cwd || '', 'plugins')
if (fs.existsSync(plugins_path)) {
const notifier = require('../../notifier')
const pluginsFile = fs.readdirSync(plugins_path).filter(e => path.extname(e).toLowerCase() === '.js')
pluginsFile.forEach( pluginFile => {
try {
const plugin = require(path.resolve(plugins_path, pluginFile))
if (typeof plugin.load !== 'function') return
plugin.load({ settings: settingsController.settings })
log.info(`Plugin ${pluginFile} loaded!`)
if (typeof plugin.onEventCreate === 'function') {
notifier.emitter.on('Create', plugin.onEventCreate)
} }
}) if (typeof plugin.onEventDelete === 'function') {
notifier.emitter.on('Delete', plugin.onEventDelete)
await settingsController.set('publicKey', publicKey) }
await settingsController.set('privateKey', privateKey, true) if (typeof plugin.onEventUpdate === 'function') {
} notifier.emitter.on('Update', plugin.onEventUpdate)
}
// initialize user_locale } catch (e) {
if (config.user_locale && fs.existsSync(path.resolve(config.user_locale))) { log.warn(`Unable to load plugin ${pluginFile}: ${String(e)}`)
const user_locale = fs.readdirSync(path.resolve(config.user_locale)) }
user_locale.forEach(async f => { })
consola.info(`Loading user locale ${f}`)
const locale = path.basename(f, '.js')
settingsController.user_locale[locale] =
(await require(path.resolve(config.user_locale, f))).default
})
}
} }
}, },
async set (key, value, is_secret = false) { async set (key, value, is_secret = false) {
const Setting = require('../models/setting')
log.info(`SET ${key} ${is_secret ? '*****' : value}`) log.info(`SET ${key} ${is_secret ? '*****' : value}`)
try { try {
const [setting, created] = await Setting.findOrCreate({ const [setting, created] = await Setting.findOrCreate({
@ -103,7 +146,7 @@ const settingsController = {
settingsController[is_secret ? 'secretSettings' : 'settings'][key] = value settingsController[is_secret ? 'secretSettings' : 'settings'][key] = value
return true return true
} catch (e) { } catch (e) {
log.error(e) log.error('[SETTING SET]', e)
return false return false
} }
}, },
@ -114,6 +157,19 @@ const settingsController = {
if (ret) { res.sendStatus(200) } else { res.sendStatus(400) } if (ret) { res.sendStatus(200) } else { res.sendStatus(400) }
}, },
async testSMTP (req, res) {
const smtp = req.body
await settingsController.set('smtp', smtp.smtp)
const mail = require('../mail')
try {
await mail._send(settingsController.settings.admin_email, 'test', null, 'en')
return res.sendStatus(200)
} catch (e) {
console.error(e)
return res.status(400).send(String(e))
}
},
setLogo (req, res) { setLogo (req, res) {
if (!req.file) { if (!req.file) {
settingsController.set('logo', false) settingsController.set('logo', false)
@ -124,16 +180,13 @@ const settingsController = {
const baseImgPath = path.resolve(config.upload_path, 'logo') const baseImgPath = path.resolve(config.upload_path, 'logo')
// convert and resize to png // convert and resize to png
sharp(uploadedPath) return sharp(uploadedPath)
.resize(400) .resize(400)
.png({ quality: 90 }) .png({ quality: 90 })
.toFile(baseImgPath + '.png', async (err, info) => { .toFile(baseImgPath + '.png', (err, info) => {
if (err) { if (err) {
log.error(err) log.error('[LOGO] ' + err)
} }
const image = await readFile(baseImgPath + '.png')
const favicon = await toIco([image], { sizes: [64], resize: true })
writeFile(baseImgPath + '.ico', favicon)
settingsController.set('logo', baseImgPath) settingsController.set('logo', baseImgPath)
res.sendStatus(200) res.sendStatus(200)
}) })
@ -141,14 +194,7 @@ const settingsController = {
getAllRequest (req, res) { getAllRequest (req, res) {
// get public settings and public configuration // get public settings and public configuration
const settings = { res.json({ ...settingsController.settings, version: pkg.version })
...settingsController.settings,
baseurl: config.baseurl,
title: config.title,
description: config.description,
version: pkg.version
}
res.json(settings)
} }
} }

View file

@ -0,0 +1,86 @@
const URL = require('url')
const helpers = require('../../helpers.js')
const log = require('../../log')
const db = require('../models/index.js')
const config = require('../../config')
const settingsController = require('./settings')
const path = require('path')
const setupController = {
async setupDb (req, res, next) {
log.debug('[SETUP] Check db')
const dbConf = req.body.db
if (!dbConf) {
return res.sendStatus(400)
}
if (dbConf.storage) {
dbConf.storage = path.resolve(process.env.cwd || '', dbConf.storage)
}
try {
// try to connect
dbConf.logging = false
await db.connect(dbConf)
// is empty ?
const isEmpty = await db.isEmpty()
if (!isEmpty) {
log.warn(' ⚠ Non empty db! Please move your current db elsewhere than retry.')
return res.status(400).send(' ⚠ Non empty db! Please move your current db elsewhere than retry.')
}
await db.runMigrations()
config.db = dbConf
config.firstrun = false
config.db.logging = false
config.baseurl = req.protocol + '://' + req.headers.host
config.hostname = new URL.URL(config.baseurl).hostname
const settingsController = require('./settings')
await settingsController.load()
return res.sendStatus(200)
} catch (e) {
return res.status(400).send(String(e))
}
},
async restart (req, res) {
try {
// write configuration
config.write()
// calculate default settings values
await settingsController.set('theme.is_dark', true)
await settingsController.set('instance_name', settingsController.settings.title.toLowerCase().replace(/ /g, ''))
// create admin
const password = helpers.randomString()
const email = `admin`
const User = require('../models/user')
await User.create({
email,
password,
is_admin: true,
is_active: true
})
res.json({ password, email })
log.info('Restart needed')
res.end()
// exit process so pm2 || docker could restart me || service
process.kill(process.pid)
} catch (e) {
log.error(String(e))
return res.status(400).send(String(e))
}
}
}
module.exports = setupController

View file

@ -1,6 +1,6 @@
const crypto = require('crypto') const crypto = require('crypto')
const { Op } = require('sequelize') const { Op } = require('sequelize')
const config = require('config') const config = require('../../config')
const mail = require('../mail') const mail = require('../mail')
const User = require('../models/user') const User = require('../models/user')
const settingsController = require('./settings') const settingsController = require('./settings')
@ -26,7 +26,7 @@ const userController = {
if (!recover_code) { return res.sendStatus(400) } if (!recover_code) { return res.sendStatus(400) }
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } }) const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
if (!user) { return res.sendStatus(400) } if (!user) { return res.sendStatus(400) }
res.sendStatus(200) res.json({ email: user.email })
}, },
async updatePasswordWithRecoverCode (req, res) { async updatePasswordWithRecoverCode (req, res) {
@ -50,7 +50,7 @@ const userController = {
}, },
async getAll (req, res) { async getAll (req, res) {
const users = await User.scope('withoutPassword').findAll({ const users = await User.scope(req.user.is_admin ? 'withRecover' : 'withoutPassword').findAll({
order: [['is_admin', 'DESC'], ['createdAt', 'DESC']] order: [['is_admin', 'DESC'], ['createdAt', 'DESC']]
}) })
res.json(users) res.json(users)
@ -100,10 +100,10 @@ const userController = {
const user = await User.create(req.body) const user = await User.create(req.body)
log.info(`Sending registration email to ${user.email}`) log.info(`Sending registration email to ${user.email}`)
mail.send(user.email, 'register', { user, config }, req.settings.locale) mail.send(user.email, 'register', { user, config }, req.settings.locale)
mail.send(config.admin_email, 'admin_register', { user, config }) mail.send(settingsController.settings.admin_email, 'admin_register', { user, config })
res.sendStatus(200) res.sendStatus(200)
} catch (e) { } catch (e) {
log.error('Registration error: "%s"', e) log.error('Registration error:', e)
res.status(404).json(e) res.status(404).json(e)
} }
}, },
@ -112,11 +112,11 @@ const userController = {
try { try {
req.body.is_active = true req.body.is_active = true
req.body.recover_code = crypto.randomBytes(16).toString('hex') req.body.recover_code = crypto.randomBytes(16).toString('hex')
const user = await User.create(req.body) const user = await User.scope('withRecover').create(req.body)
mail.send(user.email, 'user_confirm', { user, config }, req.settings.locale) mail.send(user.email, 'user_confirm', { user, config }, req.settings.locale)
res.json(user) res.json(user)
} catch (e) { } catch (e) {
log.error('User creation error: %s', e) log.error('User creation error:', e)
res.status(404).json(e) res.status(404).json(e)
} }
}, },
@ -124,10 +124,11 @@ const userController = {
async remove (req, res) { async remove (req, res) {
try { try {
const user = await User.findByPk(req.params.id) const user = await User.findByPk(req.params.id)
user.destroy() await user.destroy()
log.warn(`User ${user.email} removed!`)
res.sendStatus(200) res.sendStatus(200)
} catch (e) { } catch (e) {
log.error('User removal error: "%s"', e) log.error('User removal error:"', e)
res.status(404).json(e) res.status(404).json(e)
} }
} }

View file

@ -2,150 +2,166 @@ const express = require('express')
const multer = require('multer') const multer = require('multer')
const cors = require('cors')() const cors = require('cors')()
const { isAuth, isAdmin } = require('./auth') const config = require('../config')
const eventController = require('./controller/event')
const exportController = require('./controller/export')
const userController = require('./controller/user')
const settingsController = require('./controller/settings')
const instanceController = require('./controller/instance')
const apUserController = require('./controller/ap_user')
const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth')
const announceController = require('./controller/announce')
const helpers = require('../helpers')
const storage = require('./storage')
const upload = multer({ storage })
const config = require('config')
const log = require('../log') const log = require('../log')
const api = express.Router() const api = express.Router()
api.use(express.urlencoded({ extended: false })) api.use(express.urlencoded({ extended: false }))
api.use(express.json()) api.use(express.json())
/**
* Get current authenticated user if (config.firstrun) {
* @category User
* @name /api/user const setupController = require('./controller/setup')
* @type GET const settingsController = require('./controller/settings')
* @example **Response** api.post('/settings', settingsController.setRequest)
* ```json api.post('/setup/db', setupController.setupDb)
{ api.post('/setup/restart', setupController.restart)
"description" : null, api.post('/settings/smtp', settingsController.testSMTP)
"recover_code" : "",
"id" : 1, } else {
"createdAt" : "2020-01-29T18:10:16.630Z",
"updatedAt" : "2020-01-30T22:42:14.789Z", const { isAuth, isAdmin } = require('./auth')
"is_active" : true, const eventController = require('./controller/event')
"settings" : "{}", const settingsController = require('./controller/settings')
"email" : "eventi@cisti.org", const exportController = require('./controller/export')
"is_admin" : true const userController = require('./controller/user')
const instanceController = require('./controller/instance')
const apUserController = require('./controller/ap_user')
const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth')
const announceController = require('./controller/announce')
const helpers = require('../helpers')
const storage = require('./storage')
const upload = multer({ storage })
/**
* Get current authenticated user
* @category User
* @name /api/user
* @type GET
* @example **Response**
* ```json
{
"description" : null,
"recover_code" : "",
"id" : 1,
"createdAt" : "2020-01-29T18:10:16.630Z",
"updatedAt" : "2020-01-30T22:42:14.789Z",
"is_active" : true,
"settings" : "{}",
"email" : "eventi@cisti.org",
"is_admin" : true
}
```
*/
api.get('/ping', (req, res) => res.sendStatus(200))
api.get('/user', isAuth, (req, res) => res.json(req.user))
api.post('/user/recover', userController.forgotPassword)
api.post('/user/check_recover_code', userController.checkRecoverCode)
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
// register and add users
api.post('/user/register', userController.register)
api.post('/user', isAdmin, userController.create)
// update user
api.put('/user', isAuth, userController.update)
// delete user
api.delete('/user/:id', isAdmin, userController.remove)
api.delete('/user', isAdmin, userController.remove)
// get all users
api.get('/users', isAdmin, userController.getAll)
// update a place (modify address..)
api.put('/place', isAdmin, eventController.updatePlace)
/**
* Add a new event
* @category Event
* @name /event
* @type POST
* @info `Content-Type` has to be `multipart/form-data` to support image upload
* @param {string} title - event's title
* @param {string} description - event's description (html accepted and sanitized)
* @param {string} place_name - the name of the place
* @param {string} [place_address] - the address of the place
* @param {integer} start_datetime - start timestamp
* @param {integer} multidate - is a multidate event?
* @param {array} tags - List of tags
* @param {object} [recurrent] - Recurrent event details
* @param {string} [recurrent.frequency] - could be `1w` or `2w`
* @param {string} [recurrent.type] - not used
* @param {array} [recurrent.days] - array of days
* @param {image} [image] - Image
*/
// allow anyone to add an event (anon event has to be confirmed, TODO: flood protection)
api.post('/event', upload.single('image'), eventController.add)
api.put('/event', isAuth, upload.single('image'), eventController.update)
api.get('/event/import', isAuth, helpers.importURL)
// remove event
api.delete('/event/:id', isAuth, eventController.remove)
// get tags/places
api.get('/event/meta', eventController.getMeta)
// get unconfirmed events
api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed)
// add event notification TODO
api.post('/event/notification', eventController.addNotification)
api.delete('/event/notification/:code', eventController.delNotification)
api.get('/settings', settingsController.getAllRequest)
api.post('/settings', isAdmin, settingsController.setRequest)
api.post('/settings/logo', isAdmin, multer({ dest: config.upload_path }).single('logo'), settingsController.setLogo)
api.post('/settings/smtp', isAdmin, settingsController.testSMTP)
// confirm event
api.put('/event/confirm/:event_id', isAuth, eventController.confirm)
api.put('/event/unconfirm/:event_id', isAuth, eventController.unconfirm)
// get event
api.get('/event/:event_slug.:format?', cors, eventController.get)
// export events (rss/ics)
api.get('/export/:type', cors, exportController.export)
// get events in this range
api.get('/events', cors, eventController.select)
api.get('/instances', isAdmin, instanceController.getAll)
api.get('/instances/:instance_domain', isAdmin, instanceController.get)
api.post('/instances/toggle_block', isAdmin, instanceController.toggleBlock)
api.post('/instances/toggle_user_block', isAdmin, apUserController.toggleBlock)
api.put('/resources/:resource_id', isAdmin, resourceController.hide)
api.delete('/resources/:resource_id', isAdmin, resourceController.remove)
api.get('/resources', isAdmin, resourceController.getAll)
// - ADMIN ANNOUNCEMENTS
api.get('/announcements', isAdmin, announceController.getAll)
api.post('/announcements', isAdmin, announceController.add)
api.put('/announcements/:announce_id', isAdmin, announceController.update)
api.delete('/announcements/:announce_id', isAdmin, announceController.remove)
// OAUTH
api.get('/clients', isAuth, oauthController.getClients)
api.get('/client/:client_id', isAuth, oauthController.getClient)
api.post('/client', oauthController.createClient)
} }
```
*/
api.get('/user', isAuth, (req, res) => res.json(req.user))
api.post('/user/recover', userController.forgotPassword)
api.post('/user/check_recover_code', userController.checkRecoverCode)
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
// register and add users
api.post('/user/register', userController.register)
api.post('/user', isAdmin, userController.create)
// update user
api.put('/user', isAuth, userController.update)
// delete user
api.delete('/user/:id', isAdmin, userController.remove)
api.delete('/user', isAdmin, userController.remove)
// get all users
api.get('/users', isAdmin, userController.getAll)
// update a place (modify address..)
api.put('/place', isAdmin, eventController.updatePlace)
/**
* Add a new event
* @category Event
* @name /event
* @type POST
* @info `Content-Type` has to be `multipart/form-data` to support image upload
* @param {string} title - event's title
* @param {string} description - event's description (html accepted and sanitized)
* @param {string} place_name - the name of the place
* @param {string} [place_address] - the address of the place
* @param {integer} start_datetime - start timestamp
* @param {integer} multidate - is a multidate event?
* @param {array} tags - List of tags
* @param {object} [recurrent] - Recurrent event details
* @param {string} [recurrent.frequency] - could be `1w` or `2w`
* @param {string} [recurrent.type] - not used
* @param {array} [recurrent.days] - array of days
* @param {image} [image] - Image
*/
// allow anyone to add an event (anon event has to be confirmed, TODO: flood protection)
api.post('/event', upload.single('image'), eventController.add)
api.put('/event', isAuth, upload.single('image'), eventController.update)
api.get('/event/import', isAuth, helpers.importURL)
// remove event
api.delete('/event/:id', isAuth, eventController.remove)
// get tags/places
api.get('/event/meta', eventController.getMeta)
// get unconfirmed events
api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed)
// add event notification TODO
api.post('/event/notification', eventController.addNotification)
api.delete('/event/notification/:code', eventController.delNotification)
api.get('/settings', settingsController.getAllRequest)
api.post('/settings', isAdmin, settingsController.setRequest)
api.post('/settings/logo', isAdmin, multer({ dest: config.upload_path }).single('logo'), settingsController.setLogo)
// confirm event
api.put('/event/confirm/:event_id', isAuth, eventController.confirm)
api.put('/event/unconfirm/:event_id', isAuth, eventController.unconfirm)
// get event
api.get('/event/:event_id.:format?', cors, eventController.get)
// export events (rss/ics)
api.get('/export/:type', cors, exportController.export)
// get events in this range
api.get('/events', cors, eventController.select)
api.get('/instances', isAdmin, instanceController.getAll)
api.get('/instances/:instance_domain', isAdmin, instanceController.get)
api.post('/instances/toggle_block', isAdmin, instanceController.toggleBlock)
api.post('/instances/toggle_user_block', isAdmin, apUserController.toggleBlock)
api.put('/resources/:resource_id', isAdmin, resourceController.hide)
api.delete('/resources/:resource_id', isAdmin, resourceController.remove)
api.get('/resources', isAdmin, resourceController.getAll)
// - ADMIN ANNOUNCEMENTS
api.get('/announcements', isAdmin, announceController.getAll)
api.post('/announcements', isAdmin, announceController.add)
api.put('/announcements/:announce_id', isAdmin, announceController.update)
api.delete('/announcements/:announce_id', isAdmin, announceController.remove)
// OAUTH
api.get('/clients', isAuth, oauthController.getClients)
api.get('/client/:client_id', isAuth, oauthController.getClient)
api.post('/client', oauthController.createClient)
api.use((req, res) => res.sendStatus(404)) api.use((req, res) => res.sendStatus(404))
// Handle 500 // Handle 500
api.use((error, req, res, next) => { api.use((error, req, res, next) => {
log.error(error) log.error('[API ERROR]', error)
res.status(500).send('500: Internal Server Error') res.status(500).send('500: Internal Server Error')
}) })

View file

@ -1,7 +1,6 @@
const Email = require('email-templates') const Email = require('email-templates')
const path = require('path') const path = require('path')
const moment = require('dayjs') const moment = require('dayjs')
const config = require('config')
const settingsController = require('./controller/settings') const settingsController = require('./controller/settings')
const log = require('../log') const log = require('../log')
const { Task, TaskManager } = require('../taskManager') const { Task, TaskManager } = require('../taskManager')
@ -9,7 +8,11 @@ const locales = require('../../locales')
const mail = { const mail = {
send (addresses, template, locals, locale = settingsController.settings.instance_locale) { send (addresses, template, locals, locale = settingsController.settings.instance_locale) {
log.debug('Enqueue new email ', template, locale) if (process.env.NODE_ENV === 'production' && (!settingsController.settings.admin_email || !settingsController.settings.smtp)) {
log.error(`Cannot send any email: SMTP Email configuration not completed!`)
return
}
log.debug(`Enqueue new email ${template} ${locale}`)
const task = new Task({ const task = new Task({
name: 'MAIL', name: 'MAIL',
method: mail._send, method: mail._send,
@ -18,7 +21,8 @@ const mail = {
TaskManager.add(task) TaskManager.add(task)
}, },
_send (addresses, template, locals, locale) { _send (addresses, template, locals, locale = settingsController.settings.instance_locale) {
const settings = settingsController.settings
log.info(`Send ${template} email to ${addresses} with locale ${locale}`) log.info(`Send ${template} email to ${addresses} with locale ${locale}`)
const email = new Email({ const email = new Email({
views: { root: path.join(__dirname, '..', 'emails') }, views: { root: path.join(__dirname, '..', 'emails') },
@ -31,7 +35,7 @@ const mail = {
} }
}, },
message: { message: {
from: `📅 ${config.title} <${config.admin_email}>` from: `📅 ${settings.title} <${settings.admin_email}>`
}, },
send: true, send: true,
i18n: { i18n: {
@ -39,29 +43,29 @@ const mail = {
objectNotation: true, objectNotation: true,
syncFiles: false, syncFiles: false,
updateFiles: false, updateFiles: false,
defaultLocale: settingsController.settings.instance_locale || 'en', defaultLocale: settings.instance_locale || 'en',
locale, locale,
locales: Object.keys(locales) locales: Object.keys(locales)
}, },
transport: config.smtp transport: settings.smtp || {}
}) })
const msg = { const msg = {
template, template,
message: { message: {
to: addresses, to: addresses
bcc: config.admin_email
}, },
locals: { locals: {
...locals, ...locals,
locale, locale,
config: { title: config.title, baseurl: config.baseurl, description: config.description, admin_email: config.admin_email }, config: { title: settings.title, baseurl: settings.baseurl, description: settings.description, admin_email: settings.admin_email },
datetime: datetime => moment.unix(datetime).locale(locale).format('ddd, D MMMM HH:mm') datetime: datetime => moment.unix(datetime).locale(locale).format('ddd, D MMMM HH:mm')
} }
} }
return email.send(msg) return email.send(msg)
.catch(e => { .catch(e => {
log.error('Error sending email => %s', e) log.error('[MAIL]', e)
throw e
}) })
} }
} }

View file

@ -1,4 +1,4 @@
const sequelize = require('./index') const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize') const { Model, DataTypes } = require('sequelize')
class Announcement extends Model {} class Announcement extends Model {}

Some files were not shown because too many files have changed in this diff Show more