From f4c171c49b0e7bed050eaca23faa800802fa161c Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 29 Nov 2022 16:38:47 +0100 Subject: [PATCH 001/118] no hans --- locales/email/zh_Hans.json | 1 - locales/zh_Hans.json | 293 ------------------------------------- 2 files changed, 294 deletions(-) delete mode 100644 locales/email/zh_Hans.json delete mode 100644 locales/zh_Hans.json diff --git a/locales/email/zh_Hans.json b/locales/email/zh_Hans.json deleted file mode 100644 index 0967ef42..00000000 --- a/locales/email/zh_Hans.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json deleted file mode 100644 index f4c5b3be..00000000 --- a/locales/zh_Hans.json +++ /dev/null @@ -1,293 +0,0 @@ -{ - "common": { - "embed_help": "将以下代码复制并粘贴到你的网站,此事件将像这样显示", - "follow": "关注", - "add_event": "添加事件", - "send": "发送", - "where": "地点", - "address": "地址", - "when": "时间", - "what": "事件", - "media": "媒体", - "login": "登录", - "email": "电子邮箱", - "register": "注册", - "description": "描述", - "hide": "隐藏", - "search": "搜索", - "confirm": "确认", - "admin": "管理员", - "users": "用户", - "events": "事件", - "actions": "操作", - "deactivate": "取消", - "remove_admin": "移除管理员", - "activate": "激活", - "save": "该村", - "logout": "登出", - "name": "名称", - "associate": "合作者", - "add": "添加", - "recover_password": "重置密码", - "enable": "启用", - "me": "你", - "ok": "完成", - "resources": "资源", - "n_resources": "无资源|1 个资源|{n} 个资源", - "displayname": "显示名称", - "copy_link": "复制链接", - "send_via_mail": "发送电子邮件", - "embed": "嵌入式页面", - "feed_url_copied": "在你的 RSS 阅读器中打开复制的链接", - "follow_me_title": "在 Fediverse 网络中关注更新", - "feed": "RSS 源", - "moderation": "中等", - "authorize": "认证", - "title": "标题", - "filter": "筛选", - "pause": "暂停", - "start": "开始", - "announcements": "公告", - "url": "URL", - "place": "地点", - "theme": "主题", - "label": "标签", - "collections": "收藏", - "max_events": "最大事件数", - "next": "下一个", - "export": "导出", - "remove": "移除", - "settings": "选项", - "logout_ok": "已登出", - "new_password": "新密码", - "new_user": "新用户", - "places": "地点", - "edit": "编辑", - "cancel": "取消", - "password": "密码", - "info": "信息", - "preview": "预览", - "share": "分享", - "edit_event": "编辑事件", - "copy": "复制", - "related": "相关", - "set_password": "设置密码", - "instances": "实例", - "activate_user": "已确认", - "federation": "联盟", - "add_to_calendar": "添加到日历", - "copied": "已复制", - "embed_title": "在你的网页上嵌入此事件", - "user": "用户", - "event": "事件", - "fediverse": "Fediverse 网络", - "skip": "跳过", - "delete": "移除", - "import": "导入", - "tags": "标签", - "close": "关闭", - "disable": "禁用", - "password_updated": "密码已修改。", - "reset": "重置" - }, - "export": { - "list_description": "如果你有一个网站并希望展示一个事件列表,使用以下代码", - "email_description": "你可以通过发给你的电子邮件了解你感兴趣的事件。", - "insert_your_address": "输入你的电子邮箱地址", - "ical_description": "电脑和智能手机通常预装了能够导入远程日历的日历应用。", - "intro": "与那些竭尽全力保留用户和数据的非社交平台不同,我们认为信息和人一样,都必须是自由的。因此,即使不通过此网站,你仍可随时了解你想了解的事件之最新情况。", - "feed_description": "要想从电脑或智能手机上关注更新,而无需定期打开本网站,请使用RSS订阅。

\n\n

通过RSS订阅,您可以使用一个特殊的应用程序来接收你感兴趣的网站的更新。这是一个快速关注许多网站的好方法,不需要创建账户或做其他事。

\n\n
  • 如果您使用 Android,我们推荐 Flym 或 Feeder。
  • \n
  • 对于iPhone/iPad,您可以使用 Feed4U
  • \n
  • 对于台式机/笔记本电脑,我们推荐可在 FirefoxChrome 上安装的 Feedbro。
  • \n
    \n将此链接添加到你的RSS阅读器中,以获得最新信息。" - }, - "register": { - "description": "社会运动应该组织起来,并自筹资金
    \n
    在你能发布内容之前,你的账户必须审核通过,考虑到你可以通过此网站发现现实中的人,请写一些东西告诉我们你希望发布什么。", - "error": "错误: ", - "complete": "注册必须经过确认。", - "first_user": "管理员已创建" - }, - "event": { - "anon_description": "即使不登录或注册,你也可以创建事件,但必须等待一些人看到它,\n并确认这是一个合适的事件。此外你也不能够修改它。

    \n你也可以 登录注册。或者继续浏览以得到答案。 ", - "anon": "匿名", - "same_day": "在同一天", - "what_description": "标题", - "description_description": "描述", - "added": "事件已添加", - "saved": "事件已保存", - "added_anon": "事件已添加,等待确认。", - "updated": "事件已更新", - "where_description": "事件的地点在哪里?如果不存在,你可以创建一个。", - "confirmed": "事件已确认", - "not_found": "找不到事件", - "recurrent": "日常事件", - "edit_recurrent": "编辑日常事件:", - "show_recurrent": "日常事件", - "show_past": "以及过往的事件", - "multidate_description": "这是一个节日吗?选择它开始和结束的时间", - "multidate": "更多日期", - "normal_description": "选择日期。", - "recurrent_2w_days": "每 {days} 天一次", - "each_week": "每周", - "each_2w": "隔周一次", - "due": "直到", - "from": "来自", - "image_too_big": "图片不能大于 4MB", - "interact_with_me_at": "在 Fediverse 网络上与我互动", - "interact_with_me": "关注我", - "remove_recurrent_confirmation": "你确定要移除这个日常事件吗?\n过去的事件仍将被维护,但不会再添加新事件。", - "import_URL": "从 URL 导入", - "import_ICS": "从 ICS 导入", - "ics": "ICS", - "alt_text_description": "为视觉障碍者提供的说明", - "choose_focal_point": "选择联络点", - "download_flyer": "下载传单", - "tag_description": "标签", - "media_description": "你可以添加一份传单(可选)", - "recurrent_description": "选择频率和日期", - "only_future": "仅限即将到来的事件", - "normal": "普通", - "recurrent_1w_days": "每 {days} 天", - "recurrent_1m_days": "|每月的第 {days} 天|每月的第 {days} 天", - "recurrent_1m_ordinal": "每月的第 {n} 个 {days}", - "each_month": "每月", - "follow_me_description": "一种对这里发布的 {title} 事件保持关注的方法,\n是在 Fediverse 网络,比如 Mastodon,上关注 {account},亦有可能通过此方式给此事件添加资源。

    \n如果你没听说过 Mastodon 和 Fediverse,我们建议你阅读 这篇文章

    在下方输入你的实例名称(例如:mastodon.social)", - "import_description": "你可以从其他平台和实例通过标准格式(ICS 和 hCalendar)导入事件", - "remove_media_confirmation": "你确认要删除图片吗?", - "remove_confirmation": "你确定要移除此事件吗?" - }, - "login": { - "check_email": "检查你的电子邮箱收件箱和垃圾邮件箱。", - "not_registered": "还未注册?", - "forgot_password": "忘记密码了?", - "insert_email": "输入你的电子邮箱地址", - "error": "无法登录,检查你的登录信息。", - "ok": "已登录", - "description": "登录以发布新事件。" - }, - "recover": { - "not_valid_code": "发生了一些错误。" - }, - "admin": { - "delete_user": "移除", - "remove_admin": "移除管理员", - "disable_user_confirm": "你确定禁用 {user} 吗?", - "disable_admin_user_confirm": "你确定移除 {user} 的管理员权限吗?", - "user_remove_ok": "用户已移除", - "user_create_ok": "用户已创建", - "event_remove_ok": "事件已移除", - "allow_registration_description": "允许公众注册?", - "allow_anon_event": "允许发布匿名事件(需要确认)?", - "allow_recurrent_event": "允许日常事件", - "federation": "联邦社交网络 / ActivityPub", - "enable_federation": "启用联邦社交网络", - "add_instance": "添加实例", - "select_instance_timezone": "时区", - "enable_resources": "启用资源", - "hide_boost_bookmark": "隐藏助力/书签", - "block": "屏蔽", - "unblock": "解除屏蔽", - "instance_name": "实例名称", - "hide_resource": "隐藏资源", - "delete_resource_confirm": "你确定要删除此资源吗?", - "filter_instances": "筛选实例", - "resources": "资源", - "favicon": "图标", - "user_block_confirm": "你确定要屏蔽用户 {user} 吗?", - "delete_announcement_confirm": "你确定要移除这个公告吗?", - "announcement_remove_ok": "公告已移除", - "instance_locale": "默认语言", - "title_description": "这将被用作页面的标题和电子邮件的主题,以导出 RSS 和 ICS 源。", - "description_description": "在标题旁的页眉中显示", - "instance_place_help": "在其他的实例中显示的标签", - "delete_trusted_instance_confirm": "你确定要从友好实例菜单中删除此项目吗?", - "edit_place": "编辑地点", - "new_announcement": "新公告", - "show_smtp_setup": "电子邮件设置", - "smtp_port": "SMTP 端口", - "smtp_secure": "SMTP 安全协议(TLS 或 STARTTLS)", - "smtp_test_success": "一封测试邮件已发送至 {admin_mail},请检查你的收件箱", - "sender_email": "发件人", - "widget": "小组件", - "wrong_domain_warning": "在 config.json 中设置的 baseurl {baseurl} 与你正在访问的 {url} 不同", - "edit_collection": "编辑收藏", - "event_confirm_description": "你可以在此确认匿名用户提交的事件", - "recurrent_event_visible": "默认显示日常事件", - "place_description": "如果你弄错了地点或地址,你可以修改。
    与这个地点相关的当前和过去的所有事件都会改变地址。", - "delete_user_confirm": "你确定移除 {user} 吗?", - "enable_admin_user_confirm": "你确定授予 {user} 管理员权限吗", - "enable_federation_help": "这将允许从 Fediverse 上关注此实例", - "enable_resources_help": "允许从 Fediverse 为此事件添加资源", - "hide_boost_bookmark_help": "隐藏来自 Fediverse 上的助力和书签数量图标", - "block_user": "屏蔽用户", - "filter_users": "筛选用户", - "user_add_help": "一封带有确认订阅和设置密码指引的邮件将被发送给新用户", - "show_resource": "显示资源", - "delete_resource": "删除资源", - "user_blocked": "用户 {user} 已屏蔽", - "instance_block_confirm": "你确定要屏蔽实例 {instance} 吗?", - "announcement_description": "你可以在此段落插入显示于首页的公告", - "instance_timezone_description": "Gancio 被设计用来收集特定区域,比如一座城市所发生的事件。这里的所有事件将以所选择的时区显示。", - "instance_locale_description": "特定页面偏好的用户语言。有时信息必须以相同的语言对所有人显示(比如通过 ActivityPub 发布内容或发送一些电子邮件时)。在这种情况下将使用上面选择的语言。", - "instance_place": "该实例的指示性位置", - "trusted_instances_help": "友好实例的列表将被显示于页眉", - "footer_links": "页脚链接", - "smtp_description": "", - "smtp_use_sendmail": "使用 sendmail", - "smtp_test_button": "发送测试邮件", - "new_collection": "新建收藏", - "collections_description": "收藏是按标签和地点分组的事件。它们将于主页上显示", - "enable_trusted_instances": "启用友好实例", - "add_trusted_instance": "添加一个友好实例", - "add_link": "添加链接", - "delete_footer_link_confirm": "确定移除此链接吗?", - "instance_name_help": "要关注的 ActivityPub 账号", - "is_dark": "暗色主题", - "smtp_hostname": "SMTP 主机名" - }, - "auth": { - "fail": "无法登录。你确定密码正确吗?", - "not_confirmed": "尚未确认……" - }, - "settings": { - "change_password": "修改密码", - "password_updated": "密码已修改。", - "remove_account_confirm": "你即将永久删除你的账号", - "update_confirm": "你希望保存你的修改吗?", - "remove_account": "按下下方的按钮后你的账号将被删除。你发布的事件不会删除。", - "danger_section": "危险段落" - }, - "error": { - "email_taken": "此电子邮箱地址已被使用。", - "nick_taken": "此昵称已被使用。" - }, - "confirm": { - "title": "用户确认", - "not_valid": "出现了一些错误。", - "valid": "你的账户已被确认,你现在可以 登录" - }, - "ordinal": { - "4": "第四", - "5": "第五", - "2": "第二", - "-1": "最后", - "1": "第一", - "3": "第三" - }, - "validators": { - "required": "{fieldName} 是必填项", - "email": "输入有效的电子邮箱地址" - }, - "oauth": { - "authorization_request": "应用 {app} 申请在 {instance_name} 上获得以下权限:", - "redirected_to": "在确认后你将被重定向到 {url}", - "scopes": { - "event:write": "添加与编辑你的事件" - } - }, - "setup": { - "completed_description": "

    你现在可以以以下用户登录

    用户名:{email}
    密码:{password}

    ", - "copy_password_dialog": "没错,你必须复制密码!", - "start": "开始", - "https_warning": "你正在使用 HTTP 访问,如果你切换到 HTTPS,记得在 config.json 中修改 baseurl!", - "completed": "安装完成" - }, - "about": "\n

    Gancio 是为本地社区设计的的共享日程表。

    \n " -} From 341578b82a4b749330daded7c78e71ff33975d5a Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 29 Nov 2022 23:00:42 +0100 Subject: [PATCH 002/118] fix ics uuid --- server/api/controller/export.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/api/controller/export.js b/server/api/controller/export.js index 176a6f6f..13ac9b8b 100644 --- a/server/api/controller/export.js +++ b/server/api/controller/export.js @@ -88,6 +88,7 @@ const exportController = { 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('-').map(Number) return { + uid: `${e.id}@${settings.hostname}`, start, end, title: `[${settings.title}] ${e.title}`, From aafafe8eaac8d108572a62ac2f26db1674b7d7bc Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 29 Nov 2022 23:02:50 +0100 Subject: [PATCH 003/118] improve header gradient --- components/Appbar.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Appbar.vue b/components/Appbar.vue index abbe6427..2d3da1e3 100644 --- a/components/Appbar.vue +++ b/components/Appbar.vue @@ -32,13 +32,13 @@ export default { diff --git a/components/admin/Places.vue b/components/admin/Places.vue index 78a773f5..63e14ee5 100644 --- a/components/admin/Places.vue +++ b/components/admin/Places.vue @@ -84,7 +84,8 @@ export default { { value: 'address', text: this.$t('common.address') }, { value: 'map', text: 'Map' }, { value: 'actions', text: this.$t('common.actions'), align: 'right' } - ] + ], + geocoding_provider_type: $store.state.settings.geocoding_provider_type || 'Nominatim' } }, async fetch() { @@ -124,7 +125,7 @@ export default { this.place.latitude = this.place.longitude = null } this.$emit('input', { ...this.place }) - }, + }, searchAddress: debounce(async function(ev) { const pre_searchCoordinates = ev.target.value.trim().toLowerCase() // allow pasting coordinates lat/lon and lat,lon @@ -159,7 +160,7 @@ export default { if (searchCoordinates.length) { this.loading = true - const ret = await this.$axios.$get(`placeNominatim/${searchCoordinates}`) + const ret = await this.$axios.$get(`placeOSM/${this.geocoding_provider_type}/${searchCoordinates}`) if (ret && ret.length) { this.addressList = ret.map(v => { const name = get(v.namedetails, 'alt_name', get(v.namedetails, 'name')) @@ -176,7 +177,7 @@ export default { } this.loading = false } - }, 300) + }, 300) } } diff --git a/components/admin/Settings.vue b/components/admin/Settings.vue index 82eb3fd0..b8769034 100644 --- a/components/admin/Settings.vue +++ b/components/admin/Settings.vue @@ -54,20 +54,19 @@ v-container 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') {{$t('admin.show_smtp_setup')}} - v-btn(text @click='$emit("complete")' color='primary' v-if='setup') {{$t('common.next')}} - v-icon(v-text='mdiArrowRight') v-dialog(v-model='showGeolocationConfigs' destroy-on-close max-width='700px' :fullscreen='$vuetify.breakpoint.xsOnly') - Geolocation(@close='showGeolocationConfigs = false') - + Geolocation(setup, @close='showGeolocationConfigs = false') v-card-actions v-btn(text @click='showGeolocationConfigs=true') {{$t('admin.show_geolocation_setup')}} + v-btn(text @click='$emit("complete")' color='primary' v-if='setup') {{$t('common.next')}} + v-icon(v-text='mdiArrowRight') + diff --git a/locales/en.json b/locales/en.json index ea95bbfd..44f288e2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -82,6 +82,7 @@ "url": "URL", "place": "Place", "tags": "Tags", + "tag": "Tag", "theme": "Theme", "reset": "Reset", "import": "Import", diff --git a/pages/Admin.vue b/pages/Admin.vue index 4597fa86..6ddac5bd 100644 --- a/pages/Admin.vue +++ b/pages/Admin.vue @@ -26,6 +26,11 @@ v-container.container.pa-0.pa-md-3 v-tab-item(value='places') Places + //- TAGS + v-tab(href='#tags') {{$t('common.tags')}} + v-tab-item(value='tags') + Tags + //- GEOCODING / MAPS v-tab(href='#geolocation' v-if='settings.allow_geolocation') {{$t('admin.geolocation')}} v-tab-item(value='geolocation') @@ -77,6 +82,7 @@ export default { Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'), Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'), Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'), + Tags: () => import(/* webpackChunkName: "admin" */'../components/admin/Tags'), Collections: () => import(/* webpackChunkName: "admin" */'../components/admin/Collections'), [process.client && 'Geolocation']: () => import(/* webpackChunkName: "admin" */'../components/admin/Geolocation.vue'), Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'), diff --git a/server/api/controller/tag.js b/server/api/controller/tag.js index 1f2f882b..e15679ac 100644 --- a/server/api/controller/tag.js +++ b/server/api/controller/tag.js @@ -1,6 +1,8 @@ const Tag = require('../models/tag') const Event = require('../models/event') const uniq = require('lodash/uniq') +const log = require('../../log') + const { where, fn, col, Op } = require('sequelize') const exportController = require('./export') @@ -45,6 +47,20 @@ module.exports = { } }, + + + async getAll (_req, res) { + const tags = await Tag.findAll({ + order: [[fn('COUNT', col('tag.tag')), 'DESC']], + attributes: ['tag', [fn('COUNT', col('tag.tag')), 'count']], + include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }], + group: ['tag.tag'], + raw: true, + }) + return res.json(tags) + }, + + /** * search for tags by query string * sorted by usage @@ -60,9 +76,30 @@ module.exports = { include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }], group: ['tag.tag'], limit: 10, - subQuery:false + subQuery: false }) return res.json(tags.map(t => t.tag)) + }, + + async updateTag (req, res) { + const tag = await Tag.findByPk(req.body.tag) + await tag.update(req.body) + res.json(place) + }, + + + async remove (req, res) { + log.info('Remove tag', req.params.tag) + const tagName = req.params.tag + try { + const tag = await Tag.findByPk(tagName) + await tag.destroy() + res.sendStatus(200) + } catch (e) { + log.error('Tag removal failed:', e) + res.sendStatus(404) + } } + } \ No newline at end of file diff --git a/server/api/index.js b/server/api/index.js index 812fac98..1173559a 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -162,15 +162,18 @@ if (config.status !== 'READY') { api.get('/export/:type', cors, exportController.export) - api.get('/place/all', isAdmin, placeController.getAll) + api.get('/places', isAdmin, placeController.getAll) api.get('/place/:placeName', cors, placeController.getEvents) api.get('/place', cors, placeController.search) api.get('/placeOSM/Nominatim/:place_details', cors, placeController._nominatim) api.get('/placeOSM/Photon/:place_details', cors, placeController._photon) api.put('/place', isAdmin, placeController.updatePlace) + api.get('/tags', isAdmin, tagController.getAll) api.get('/tag', cors, tagController.search) api.get('/tag/:tag', cors, tagController.getEvents) + api.delete('/tag/:tag', isAdmin, tagController.remove) + // - FEDIVERSE INSTANCES, MODERATION, RESOURCES api.get('/instances', isAdmin, instanceController.getAll) diff --git a/tests/app.test.js b/tests/app.test.js index 9ed543ff..8411c6c2 100644 --- a/tests/app.test.js +++ b/tests/app.test.js @@ -285,10 +285,10 @@ describe('Place', () => { }) test('admin should get all places', async () => { - await request(app).get('/api/place/all') + await request(app).get('/api/places') .expect(403) - const response = await request(app).get('/api/place/all') + const response = await request(app).get('/api/places') .auth(token.access_token, { type: 'bearer' }) .expect(200) From dcb954918a4d22c69b286527a173ed1d5e871558 Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 13 Dec 2022 15:37:04 +0100 Subject: [PATCH 047/118] update tag --- components/admin/Collections.vue | 2 +- components/admin/Tags.vue | 41 +++++++++++++++++++++----------- locales/en.json | 3 +++ locales/it.json | 2 ++ server/api/controller/tag.js | 18 ++++++++++++++ server/api/index.js | 3 +++ 6 files changed, 54 insertions(+), 15 deletions(-) diff --git a/components/admin/Collections.vue b/components/admin/Collections.vue index 260a796d..5dd87531 100644 --- a/components/admin/Collections.vue +++ b/components/admin/Collections.vue @@ -33,7 +33,7 @@ v-container :prepend-icon="mdiTagMultiple" chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint :disabled="!collection.id" - placeholder='Tutte' + placeholder='All' @input.native='searchTags' @focus='searchTags' :delimiters="[',', ';']" diff --git a/components/admin/Tags.vue b/components/admin/Tags.vue index e9a62111..6670ebca 100644 --- a/components/admin/Tags.vue +++ b/components/admin/Tags.vue @@ -9,19 +9,28 @@ v-container v-dialog(v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly') v-card - v-card-title {{ $t('admin.edit_tag') }} + v-card-title {{$t('admin.edit_tag')}} - + strong.ml-2 {{tag.tag}} + v-card-subtitle {{$tc('admin.edit_tag_help', tag.count)}} v-card-text + p {{newTag}} v-form(v-model='valid' ref='form' lazy-validation) - v-text-field( - :rules="[$validators.required('common.name')]" - :label="$t('common.tag')" - v-model='tag.tag' - :placeholder='$t("common.tag")') + v-combobox(v-model='newTag' + :prepend-icon="mdiTag" + hide-no-data + persistent-hint + :items="tags" + :return-object='false' + item-value='tag' + item-text='tag' + :label="$t('common.tags')") + template(v-slot:item="{ item, on, attrs }") + span "{{item.tag}}" ({{item.count}}) v-card-actions v-spacer v-btn(@click='dialog = false' outlined color='warning') {{ $t('common.cancel') }} - v-btn(@click='savePlace' color='primary' outlined :loading='loading' + v-btn(@click='saveTag' color='primary' outlined :loading='loading' :disable='!valid || loading') {{ $t('common.save') }} v-card-text @@ -44,7 +53,7 @@ v-container diff --git a/locales/en.json b/locales/en.json index ea95bbfd..b074a9f5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -82,6 +82,7 @@ "url": "URL", "place": "Place", "tags": "Tags", + "tag": "Tag", "theme": "Theme", "reset": "Reset", "import": "Import", @@ -213,6 +214,7 @@ "hide_resource": "Hide resource", "delete_resource": "Delete resource", "delete_resource_confirm": "Are you sure you want to delete this resource?", + "delete_tag_confirm": "Are you sure you want to remove the tag \"{tag}\"? The tag will be removed from {n} events.", "block_user": "Block user", "filter_instances": "Filter instances", "filter_users": "Filter users", @@ -244,6 +246,8 @@ "footer_links": "Footer links", "delete_footer_link_confirm": "Sure to remove this link?", "edit_place": "Edit place", + "edit_tag": "Edit tag", + "edit_tag_help": "You can change the tag by replacing it with a new one or merging it with an existing one. The {n} associated events will also be changed.", "new_announcement": "New announcement", "show_smtp_setup": "Email settings", "smtp_hostname": "SMTP Hostname", diff --git a/locales/it.json b/locales/it.json index 1e05c169..a8daeca3 100644 --- a/locales/it.json +++ b/locales/it.json @@ -240,6 +240,8 @@ "footer_links": "Collegamenti del piè di pagina", "delete_footer_link_confirm": "Vuoi eliminare questo collegamento?", "edit_place": "Modifica luogo", + "edit_tag": "Modifica tag", + "edit_tag_help": "Puoi cambiare il tag sostituendolo con uno nuovo o unendolo ad uno gia' esistente. Verranno modificati anche i {n} eventi associati", "new_announcement": "Nuovo annuncio", "show_smtp_setup": "Impostazioni email", "widget": "Widget", diff --git a/pages/Admin.vue b/pages/Admin.vue index 4597fa86..6ddac5bd 100644 --- a/pages/Admin.vue +++ b/pages/Admin.vue @@ -26,6 +26,11 @@ v-container.container.pa-0.pa-md-3 v-tab-item(value='places') Places + //- TAGS + v-tab(href='#tags') {{$t('common.tags')}} + v-tab-item(value='tags') + Tags + //- GEOCODING / MAPS v-tab(href='#geolocation' v-if='settings.allow_geolocation') {{$t('admin.geolocation')}} v-tab-item(value='geolocation') @@ -77,6 +82,7 @@ export default { Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'), Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'), Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'), + Tags: () => import(/* webpackChunkName: "admin" */'../components/admin/Tags'), Collections: () => import(/* webpackChunkName: "admin" */'../components/admin/Collections'), [process.client && 'Geolocation']: () => import(/* webpackChunkName: "admin" */'../components/admin/Geolocation.vue'), Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'), diff --git a/server/api/controller/tag.js b/server/api/controller/tag.js index 1f2f882b..d581179b 100644 --- a/server/api/controller/tag.js +++ b/server/api/controller/tag.js @@ -1,6 +1,8 @@ const Tag = require('../models/tag') const Event = require('../models/event') const uniq = require('lodash/uniq') +const log = require('../../log') + const { where, fn, col, Op } = require('sequelize') const exportController = require('./export') @@ -45,6 +47,20 @@ module.exports = { } }, + + + async getAll (_req, res) { + const tags = await Tag.findAll({ + order: [[fn('COUNT', col('tag.tag')), 'DESC']], + attributes: ['tag', [fn('COUNT', col('tag.tag')), 'count']], + include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }], + group: ['tag.tag'], + raw: true, + }) + return res.json(tags) + }, + + /** * search for tags by query string * sorted by usage @@ -60,9 +76,48 @@ module.exports = { include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }], group: ['tag.tag'], limit: 10, - subQuery:false + subQuery: false }) return res.json(tags.map(t => t.tag)) + }, + + async updateTag (req, res) { + const tag = await Tag.findByPk(req.body.tag) + await tag.update(req.body) + res.json(place) + }, + + async updateTag (req, res) { + const oldtag = await Tag.findByPk(req.body.tag) + const newtag = await Tag.findByPk(req.body.newTag) + + // if the new tag does not exists, just rename the old one + if (!newtag) { + oldtag.tag = req.body.newTag + await oldtag.update({ tag: req.body.newTag }) + } else { + // in case it exists: + // - search for events with old tag + const events = await oldtag.getEvents() + // - substitute it with the new one + await oldtag.removeEvents(events) + await newtag.addEvents(events) + } + res.sendStatus(200) + }, + + async remove (req, res) { + log.info('Remove tag', req.params.tag) + const tagName = req.params.tag + try { + const tag = await Tag.findByPk(tagName) + await tag.destroy() + res.sendStatus(200) + } catch (e) { + log.error('Tag removal failed:', e) + res.sendStatus(404) + } } + } \ No newline at end of file diff --git a/server/api/index.js b/server/api/index.js index 812fac98..4fbed0a9 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -162,15 +162,21 @@ if (config.status !== 'READY') { api.get('/export/:type', cors, exportController.export) - api.get('/place/all', isAdmin, placeController.getAll) + // - PLACES + api.get('/places', isAdmin, placeController.getAll) api.get('/place/:placeName', cors, placeController.getEvents) api.get('/place', cors, placeController.search) api.get('/placeOSM/Nominatim/:place_details', cors, placeController._nominatim) api.get('/placeOSM/Photon/:place_details', cors, placeController._photon) api.put('/place', isAdmin, placeController.updatePlace) + // - TAGS + api.get('/tags', isAdmin, tagController.getAll) api.get('/tag', cors, tagController.search) api.get('/tag/:tag', cors, tagController.getEvents) + api.delete('/tag/:tag', isAdmin, tagController.remove) + api.put('/tag', isAdmin, tagController.updateTag) + // - FEDIVERSE INSTANCES, MODERATION, RESOURCES api.get('/instances', isAdmin, instanceController.getAll) diff --git a/tests/app.test.js b/tests/app.test.js index 9ed543ff..8411c6c2 100644 --- a/tests/app.test.js +++ b/tests/app.test.js @@ -285,10 +285,10 @@ describe('Place', () => { }) test('admin should get all places', async () => { - await request(app).get('/api/place/all') + await request(app).get('/api/places') .expect(403) - const response = await request(app).get('/api/place/all') + const response = await request(app).get('/api/places') .auth(token.access_token, { type: 'bearer' }) .expect(200) From 84feaa2626e204ba8425c0a7956c78de9b827ed9 Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 13 Dec 2022 16:01:11 +0100 Subject: [PATCH 049/118] typo, fix #222 --- server/api/controller/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/controller/settings.js b/server/api/controller/settings.js index 9b7fd9ec..b3d1dfad 100644 --- a/server/api/controller/settings.js +++ b/server/api/controller/settings.js @@ -217,7 +217,7 @@ const settingsController = { } const uploadedPath = path.join(req.file.destination, req.file.filename) - const baseImgPath = path.resolve(config.upload_path, 'fallbackImage.png') + const baseImgPath = path.resolve(config.upload_path, 'headerImage.png') // convert and resize to png return sharp(uploadedPath) From cd2faddcf97c6c7c323b9e9545dd28fb1eabd8b5 Mon Sep 17 00:00:00 2001 From: Nacho Date: Sat, 10 Dec 2022 11:50:08 +0000 Subject: [PATCH 050/118] Translated using Weblate (Spanish) Currently translated at 95.4% (293 of 307 strings) Translation: Gancio/Web Translate-URL: https://hosted.weblate.org/projects/gancio/web/es/ --- locales/es.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index dd46a2ce..b8f74766 100644 --- a/locales/es.json +++ b/locales/es.json @@ -276,7 +276,12 @@ "created_at": "Creado a las", "blocked": "Bloqueado", "known_users": "Usuarios conocidos", - "default_images_help": "Tienes que actualizas la página para ver los cambios." + "default_images_help": "Tienes que actualizas la página para ver los cambios.", + "geocoding_provider": "Proveedor de geocodificación", + "admin_email_help": "La dirección que usamos como remitente para enviar emails. Esta es también la dirección a la cual los emails de administración son enviados", + "allow_multidate_event": "Permitir eventos de múltiples días", + "geocoding_countrycodes": "Códigos de país", + "geocoding_provider_help": "El proveedor por defecto es Nominatim" }, "auth": { "not_confirmed": "Todavía no hemos confirmado este email…", From 5e8726000e6c73e60851733ac17c610f181dcb68 Mon Sep 17 00:00:00 2001 From: Txopi Date: Sun, 11 Dec 2022 11:24:36 +0000 Subject: [PATCH 051/118] Translated using Weblate (Basque) Currently translated at 99.6% (306 of 307 strings) Translation: Gancio/Web Translate-URL: https://hosted.weblate.org/projects/gancio/web/eu/ --- locales/eu.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/locales/eu.json b/locales/eu.json index 6d2975b1..7654a816 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -273,7 +273,25 @@ "domain": "Domeinua", "default_images_help": "Orrialdea freskatu behar duzu aldaketak ikusteko.", "known_users": "Erabiltzaile ezagunak", - "created_at": "Noiz sortua:" + "created_at": "Noiz sortua:", + "geocoding_provider_type_help": "Software lehenetsia Nominatim da", + "geocoding_provider": "Geokodeketaren hornitzailea", + "geocoding_countrycodes": "Estatuen kodeak", + "geocoding_countrycodes_help": "Baimendu iragazki bat ezartzen zonalde kodearen arabera", + "geocoding_test_button": "Probatu geokodeketa", + "tilelayer_provider_help": "Lehenetsitako hornitzailea OpenStreetMap da", + "tilelayer_provider_attribution": "Atribuzioa", + "geocoding_test_error": "Geokodeketa zerbitzua {service_name}-(e)n ez dago atzigarri", + "tilelayer_provider": "Lauza-geruzen hornitzailea", + "tilelayer_test_button": "Probatu lauza-geruza", + "tilelayer_test_error": "Lauza-geruzen zerbitzua {service_name}(e)n ez dago atzigarri", + "admin_email_help": "Epostak bidaltzeko erabiltzen dugun helbidea. Administrazio epostak ere horra bidaltzen dira", + "allow_multidate_event": "Baimendu egun anitzeko ekitaldiak", + "geocoding_provider_type": "Geokodeketarako softwarea", + "geocoding_provider_help": "Lehenetsitako hornitzailea Nominatim da", + "geolocation": "Geokokapena", + "geocoding_test_success": "Geokodeketa zerbitzua {service_name}-(e)n martxan dago", + "tilelayer_test_success": "Lauza-geruzen zerbitzua {service_name}(e)n martxan dago" }, "auth": { "not_confirmed": "Oraindik baieztatu gabe dago…", From 417725835e5346408428bd3a31e1adb2e10a1932 Mon Sep 17 00:00:00 2001 From: Stephan Schildberg Date: Mon, 12 Dec 2022 10:47:38 +0000 Subject: [PATCH 052/118] Translated using Weblate (German) Currently translated at 99.0% (304 of 307 strings) Translation: Gancio/Web Translate-URL: https://hosted.weblate.org/projects/gancio/web/de/ --- locales/de.json | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/locales/de.json b/locales/de.json index 5fdb2a43..a3f897a8 100644 --- a/locales/de.json +++ b/locales/de.json @@ -97,7 +97,10 @@ "home": "Startseite", "about": "Über", "plugins": "Plugins", - "help_translate": "Hilf beim Übersetzen mit" + "help_translate": "Hilf beim Übersetzen mit", + "content": "Inhalt", + "admin_actions": "Aktionen der Administrierenden", + "recurring_event_actions": "Einstellungen für regelmäßige Veranstaltungen" }, "admin": { "delete_footer_link_confirm": "Möchtest du diesen Link löschen?", @@ -129,7 +132,7 @@ "wrong_domain_warning": "Die \"baseurl\" die in config.json konfiguriert ist ({baseurl}) unterscheidet sich von derjenigen ({url}) die du besuchst", "instance_place_help": "Diese Textzeile wird im Menü der anderen befreundeten Instanzen angezeigt", "place_description": "Falls ein Ort falsch ist oder sich die Adresse ändert, kannst du ihn ändern.
    Bitte beachte, dass alle Veranstaltungen, die mit diesem Ort verbunden sind, die Adresse ändern (auch zurückliegende).", - "enable_admin_user_confirm": "Achte darauf, dass du der nutzenden Person {user} Admin-Rechte hinzufügst?", + "enable_admin_user_confirm": "Bist du dir sicher, dass du der nutzenden Person {user} Admin-Rechte gewährst?", "trusted_instances_help": "Befreundete Instanzen werden in der Navigationsleiste oben auf der Seite angezeigt", "trusted_instances_label": "Navigationsbezeichnung zu Friend-Instanzen", "trusted_instances_label_default": "Freundliche Instanzen", @@ -184,7 +187,32 @@ "default_images": "Standard Bilder", "config_plugin": "Plugin Konfiguration", "hide_thumbs": "Vorschaubilder ausblenden", - "hide_calendar": "Kalender verstecken" + "hide_calendar": "Kalender verstecken", + "admin_email_help": "Die Adresse, die wir als Absender für den Versand von E-Mails verwenden. Sie ist auch die Adresse, an die deine E-Mails an die Administrator:innen geschickt werden.", + "blocked": "Geblockt", + "domain": "Domain", + "known_users": "Bekannte Nutzer:innen", + "created_at": "Erstellt am", + "geocoding_provider_type": "Software für Georeferenzierung", + "geocoding_provider_type_help": "Die Standard-Software ist Nominatim", + "geocoding_provider": "Anbieter von Geocodierung", + "geocoding_provider_help": "Der Standard-Anbieter ist Nominatim", + "geocoding_countrycodes": "Gebietskennziffern", + "geocoding_countrycodes_help": "Ermöglicht die Einrichtung eines Filters für die Suche auf der Grundlage von Ländercodes", + "geocoding_test_button": "Geokodierung testen", + "geocoding_test_success": "Der Geokodierdienst unter {service_name} funktioniert", + "geocoding_test_error": "Der Dienst ist unter der angegebenen Adresse nicht zu erreichen: {service_name}", + "tilelayer_provider": "Kachel-LayerAnbieter", + "tilelayer_provider_help": "Der Standard-Anbieter ist OpenStreetMap", + "tilelayer_provider_attribution": "Namensnennung", + "tilelayer_test_button": "Kachel-Layer testen", + "tilelayer_test_success": "Der Kachel-Layer-Dienst unter {service_name} funktioniert", + "tilelayer_test_error": "Der Dienst ist unter der angegebenen Adresse nicht zu erreichen: {service_name}", + "geolocation": "Geolokation", + "allow_multidate_event": "Lasse mehrtägige Veranstaltungen zu", + "admin_email": "E-Mail von der administrierenden Person", + "geolocation_description": "1. Bestimme einen Anbieter für einen Geokodierdienst.
    Derzeit gibt es unter den im Wiki von OpenStreetMap, Anbietern Unterstützung für die Software Nominatim und Photon.
    Du kannst eine der entsprechenden offiziellen Demos verwenden, indem du den Link in das Feld \"Geocoding provider\" kopierst:
    2. Definiere einen Anbieter für Kartenebenen.
    Eine Liste von ihnen findest du hier: https://leaflet-extras.github.io/leaflet-providers/preview/", + "default_images_help": "Du musst die Seite neu laden, um die Änderungen sehen zu können." }, "settings": { "update_confirm": "Willst du deine Änderung speichern?", From 04e2c3445c8f406c1b762bb7435ba7b0951d22a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Sun, 11 Dec 2022 04:34:35 +0000 Subject: [PATCH 053/118] Translated using Weblate (Galician) Currently translated at 100.0% (307 of 307 strings) Translation: Gancio/Web Translate-URL: https://hosted.weblate.org/projects/gancio/web/gl/ --- locales/gl.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/locales/gl.json b/locales/gl.json index 4513b7c1..89d23aec 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -269,7 +269,26 @@ "default_images": "Imaxes por defecto", "blocked": "Bloqueado", "domain": "Dominio", - "default_images_help": "Tes que actualizar a páxina para ver os cambios." + "default_images_help": "Tes que actualizar a páxina para ver os cambios.", + "geocoding_provider_type": "Software Geocoding", + "geocoding_provider_type_help": "O software por defecto é Nominatim", + "geocoding_provider": "Provedor Geocoding", + "geocoding_provider_help": "O provedor por defecto é Nominatim", + "geocoding_countrycodes": "Códigos de país", + "geocoding_countrycodes_help": "Permíteche establecer un filtro para as buscas en función do código", + "geocoding_test_button": "Comproba a codificación", + "geocoding_test_success": "O servizo geocoding en {service_name} funciona", + "geocoding_test_error": "O servizo geocoding non está accesible en {service_name}", + "tilelayer_provider": "Provedor de teselas do mapa", + "tilelayer_provider_help": "O provedor por defecto é OpenStreetMap", + "tilelayer_provider_attribution": "Atribución", + "tilelayer_test_button": "Comproba as capas", + "tilelayer_test_success": "O servizo de capas en {service_name} funciona", + "tilelayer_test_error": "O servizo de capas en {service_name} non está accesible", + "geolocation": "Xeolocalización", + "allow_multidate_event": "Permitir eventos de varios días", + "admin_email_help": "O enderezo que se usará como remitente para os emails. Tamén é o enderezo ao que se envían emails de administración", + "geolocation_description": "1. Define un provedor para o servizo geocoding.
    Actualmente, entre os que aparecen na wiki de OpenStreetMap, temos soporte para Nominatim e Photon.
    Podes usar unha das demos oficiais indicadas copiando a ligazón no campo 'Provedor Geocoding':
    2. Define un provedor para capas do mapa.
    Aquí hai unha lista: https://leaflet-extras.github.io/leaflet-providers/preview/" }, "auth": { "not_confirmed": "Aínda non foi confirmado…", From bf1597bcc1c8e14e8aa12e1637ab9290dda664d5 Mon Sep 17 00:00:00 2001 From: lesion Date: Thu, 15 Dec 2022 09:47:49 +0100 Subject: [PATCH 054/118] minor --- components/admin/Tags.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/admin/Tags.vue b/components/admin/Tags.vue index 6670ebca..02a0d15c 100644 --- a/components/admin/Tags.vue +++ b/components/admin/Tags.vue @@ -13,7 +13,6 @@ v-container strong.ml-2 {{tag.tag}} v-card-subtitle {{$tc('admin.edit_tag_help', tag.count)}} v-card-text - p {{newTag}} v-form(v-model='valid' ref='form' lazy-validation) v-combobox(v-model='newTag' :prepend-icon="mdiTag" @@ -94,6 +93,7 @@ export default { this.$nextTick( async () => { await this.$axios.$put('/tag', { tag: this.tag.tag, newTag: this.newTag }) await this.$fetch() + this.newTag = '' this.loading = false this.dialog = false }) From 9a08f69fe8e962e16dcb9c7725a66d2f0aca12bf Mon Sep 17 00:00:00 2001 From: lesion Date: Thu, 15 Dec 2022 09:48:19 +0100 Subject: [PATCH 055/118] update deps --- package.json | 14 ++++---- yarn.lock | 100 +++++++++++++++++++++++++-------------------------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index 9d3a8f39..38712722 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "node": ">=14 <=16" }, "dependencies": { - "@mdi/js": "^7.0.96", + "@mdi/js": "^7.1.96", "@nuxtjs/auth": "^4.9.1", "@nuxtjs/axios": "^5.13.5", "@nuxtjs/i18n": "^7.3.0", @@ -75,17 +75,17 @@ "passport-oauth2-client-password": "^0.1.2", "passport-oauth2-client-public": "^0.0.1", "pg": "^8.8.0", - "sequelize": "^6.26.0", + "sequelize": "^6.27.0", "sequelize-slugify": "^1.6.2", "sharp": "^0.27.2", - "sqlite3": "^5.0.11", + "sqlite3": "^5.1.4", "telegraf": "^4.9.1", "tiptap": "^1.32.0", "tiptap-extensions": "^1.35.0", "umzug": "^2.3.0", "v-calendar": "^2.4.1", "vue2-leaflet": "^2.7.1", - "vuetify": "2.6.12", + "vuetify": "2.6.13", "winston": "^3.8.2", "winston-daily-rotate-file": "^4.7.1", "yargs": "^17.5.0" @@ -94,12 +94,12 @@ "@nuxtjs/vuetify": "^1.12.3", "jest": "^29.3.1", "jest-environment-node": "^29.3.1", - "prettier": "^2.7.1", + "prettier": "^2.8.1", "pug": "^3.0.2", "pug-plain-loader": "^1.1.0", - "sass": "^1.56.1", + "sass": "^1.56.2", "sequelize-cli": "^6.3.0", - "supertest": "^6.3.2", + "supertest": "^6.3.3", "webpack": "4", "webpack-cli": "^4.10.0" }, diff --git a/yarn.lock b/yarn.lock index ac0978dd..195be9a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1326,10 +1326,10 @@ semver "^7.3.5" tar "^6.1.11" -"@mdi/js@^7.0.96": - version "7.0.96" - resolved "https://registry.yarnpkg.com/@mdi/js/-/js-7.0.96.tgz#8e7a54d5cb38831e3150632cc19e380f54d9edd6" - integrity sha512-lNqhkV3cpPfYb/Avh+vXLFukUTbHbyHoFo4Jdc7Oc9UvURGVhamFIpgOVvEf2bNA78zvjXTZeVWExUTR+DLBfQ== +"@mdi/js@^7.1.96": + version "7.1.96" + resolved "https://registry.yarnpkg.com/@mdi/js/-/js-7.1.96.tgz#9c5a7f8a20ea63c03b09a0dba1768fe8b70eeaf9" + integrity sha512-wlrJs6Ryhaa5CqhK3FjTfMRnb/s7HeLkKMFqwQySkK86cdN1TGdzpSM3O4tsmzCA1dYBeTbXvOwSE/Y42cUrvA== "@messageformat/core@^3.0.0": version "3.0.1" @@ -4465,10 +4465,10 @@ devalue@^2.0.1: resolved "https://registry.yarnpkg.com/devalue/-/devalue-2.0.1.tgz#5d368f9adc0928e47b77eea53ca60d2f346f9762" integrity sha512-I2TiqT5iWBEyB8GRfTDP0hiLZ0YeDJZ+upDxjBfOC2lebO5LezQMv7QvIUTzdb64jQyAKLf1AHADtGN+jw6v8Q== -dezalgo@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" - integrity sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ== +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== dependencies: asap "^2.0.0" wrappy "1" @@ -5419,15 +5419,15 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== -formidable@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" - integrity sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ== +formidable@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.1.tgz#81269cbea1a613240049f5f61a9d97731517414f" + integrity sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ== dependencies: - dezalgo "1.0.3" - hexoid "1.0.0" - once "1.4.0" - qs "6.9.3" + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" forwarded@0.2.0: version "0.2.0" @@ -5902,7 +5902,7 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -hexoid@1.0.0: +hexoid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== @@ -8694,7 +8694,7 @@ on-headers@^1.0.2, on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -9907,10 +9907,10 @@ prepend-http@^1.0.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== -prettier@^2.7.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.0.tgz#c7df58393c9ba77d6fba3921ae01faf994fb9dc9" - integrity sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA== +prettier@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" + integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== pretty-bytes@^5.6.0: version "5.6.0" @@ -10327,11 +10327,6 @@ qs@6.11.0, qs@^6.11.0: dependencies: side-channel "^1.0.4" -qs@6.9.3: - version "6.9.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" - integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== - query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -10788,10 +10783,10 @@ sass-loader@^10.2.0: schema-utils "^3.0.0" semver "^7.3.2" -sass@^1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7" - integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ== +sass@^1.56.2: + version "1.56.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.2.tgz#9433b345ab3872996c82a53a58c014fd244fd095" + integrity sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -10931,10 +10926,10 @@ sequelize-slugify@^1.6.2: dependencies: sluglife "^0.9.8" -sequelize@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.26.0.tgz#ef4a48849f20b28303d3e2c1cb0f0e13755f26ae" - integrity sha512-Xv82z1FdSn/qwB1IObSxIHV519cFk/vSD28vWs8Y0VucQLn7pK2x2jYjf2Qg/rBUQbCVprDdU7RPf+55rrkc0A== +sequelize@^6.27.0: + version "6.27.0" + resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.27.0.tgz#b267e76997df57842cc1e2c1c1d7e02405bcdb9c" + integrity sha512-Rm7BM8HQekeABup0KdtSHriu8ppJuHj2TJWCxvZtzU6j8V1LVnBk2rs38P8r4gMWgdLKs5NYoLC4il95KLsv0w== dependencies: "@types/debug" "^4.1.7" "@types/validator" "^13.7.1" @@ -11318,10 +11313,10 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -sqlite3@^5.0.11: - version "5.1.2" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.2.tgz#f50d5b1482b6972fb650daf6f718e6507c6cfb0f" - integrity sha512-D0Reg6pRWAFXFUnZKsszCI67tthFD8fGPewRddDCX6w4cYwz3MbvuwRICbL+YQjBAh9zbw+lJ/V9oC8nG5j6eg== +sqlite3@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.4.tgz#35f83d368963168b324ad2f0fffce09f3b8723a7" + integrity sha512-i0UlWAzPlzX3B5XP2cYuhWQJsTtlMD6obOa1PgeEQ4DHEXUuyJkgv50I3isqZAP5oFc2T8OFvakmDh2W6I+YpA== dependencies: "@mapbox/node-pre-gyp" "^1.0.0" node-addon-api "^4.2.0" @@ -11573,29 +11568,29 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" -superagent@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.3.tgz#15c8ec5611a1f01386994cfeeda5aa138bcb7b17" - integrity sha512-oBC+aNsCjzzjmO5AOPBPFS+Z7HPzlx+DQr/aHwM08kI+R24gsDmAS1LMfza1fK+P+SKlTAoNZpOvooE/pRO1HA== +superagent@^8.0.5: + version "8.0.6" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.6.tgz#e3fb0b3112b79b12acd605c08846253197765bf6" + integrity sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw== dependencies: component-emitter "^1.3.0" cookiejar "^2.1.3" debug "^4.3.4" fast-safe-stringify "^2.1.1" form-data "^4.0.0" - formidable "^2.0.1" + formidable "^2.1.1" methods "^1.1.2" mime "2.6.0" qs "^6.11.0" semver "^7.3.8" -supertest@^6.3.2: - version "6.3.2" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.2.tgz#7780b4b85bb2ea675c05b5cb80fa52f4dbe5a52b" - integrity sha512-mSmbW/sPpBU6K8w8189ZiHdc62zMe7dCHpC2ktS9tc0/d2DN0FaxNbDJJNFknZD4jCrGJpxkiFoVyemvKgOdwA== +supertest@^6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== dependencies: methods "^1.1.2" - superagent "^8.0.3" + superagent "^8.0.5" supports-color@^5.3.0: version "5.5.0" @@ -12513,7 +12508,12 @@ vuetify-loader@^1.7.3: file-loader "^6.2.0" loader-utils "^2.0.0" -vuetify@2.6.12, vuetify@^2.6: +vuetify@2.6.13: + version "2.6.13" + resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.6.13.tgz#6c7dd1a43b5cee0e474fbe840f0fe86439d54502" + integrity sha512-HhJi52IzhfrmWFwcYFUiA1GRIzz9smbR06Lj61Ml5HgD5PBcMiDywUnNPVid1VsXO4qWpBU6kO3O89uTxH1yzw== + +vuetify@^2.6: version "2.6.12" resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.6.12.tgz#ef1871c720834d53e00fef142b9841a3e8c1a4f9" integrity sha512-qe3hcMpWmT1O15tp+p65lOS7UKZ/hQYQktCsw9iXx2u3RwVbX6GR82gY2iROrKsiAzYDvMgrYxWQwY/pUfkekw== From f212ac1421e6e25adc8643f3341358da03d719e1 Mon Sep 17 00:00:00 2001 From: lesion Date: Thu, 15 Dec 2022 09:52:14 +0100 Subject: [PATCH 056/118] changelog v1.6.1 --- CHANGELOG | 7 +++++++ docs/changelog.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 74ac76b1..cbeb5a5c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,12 @@ All notable changes to this project will be documented in this file. +### 1.6.1 - 15 dec '22 + - allow edit tags in admin panel, fix #170 + - fix header / fallback image upload, fix #222 + - fix WPGancio MU + - fix recurrent events label + - update translations (de, es, eu, gl) + ### 1.6.0 - 11 dec '22 - new plugin system - fix #177 - new "publish on telegram" plugin: (thanks @fadelkon) diff --git a/docs/changelog.md b/docs/changelog.md index bbc097b2..660cfaa4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,13 @@ nav_order: 10 All notable changes to this project will be documented in this file. +### 1.6.1 - 15 dec '22 + - allow edit tags in admin panel, fix #170 + - fix header / fallback image upload, fix #222 + - fix WPGancio MU + - fix recurrent events label + - update translations (de, es, eu, gl) + ### 1.6.0 - 11 dec '22 - new plugin system - fix #177 - new "publish on telegram" plugin: (thanks @fadelkon) From a3e75a18306ddd6ae3c80b32cba55d15226f426f Mon Sep 17 00:00:00 2001 From: lesion Date: Thu, 15 Dec 2022 09:57:42 +0100 Subject: [PATCH 057/118] v1.6.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38712722..4e8ceceb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gancio", - "version": "1.6.0", + "version": "1.6.1", "description": "A shared agenda for local communities", "author": "lesion", "scripts": { From 32c9d27499d6eb2faa8e0e4133767d7bd17022d7 Mon Sep 17 00:00:00 2001 From: Renne Rocha Date: Thu, 15 Dec 2022 14:16:27 +0000 Subject: [PATCH 058/118] Translated using Weblate (Portuguese) Currently translated at 94.4% (290 of 307 strings) Translation: Gancio/Web Translate-URL: https://hosted.weblate.org/projects/gancio/web/pt/ --- locales/pt.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index a74aa379..c0c91e77 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -189,7 +189,10 @@ "known_users": "Usuários conhecidos", "created_at": "Criado em", "hide_calendar": "Ocultar calendário", - "blocked": "Bloqueado" + "blocked": "Bloqueado", + "admin_email": "E-mail do admin", + "tilelayer_provider_attribution": "Atribuição", + "geolocation": "Geolocalização" }, "event": { "follow_me_description": "Uma das maneiras de se manter atualizado com os eventos publicados aqui em {title},\né seguir a conta {account} no Fediverso, por exemplo via Mastodon, e possivelmente adicionar recursos para um evento a partir de lá.

    \nSe você nunca ouviu falar sobre Mastodon ou do Fediverso nós recomendamos ler este artigo.

    Entre com sua instância abaixo (e.g. mastodon.social)", From 40a9b8fd62c3e50d6cc3975e247afb030c380549 Mon Sep 17 00:00:00 2001 From: sedum Date: Tue, 20 Dec 2022 06:25:50 +0100 Subject: [PATCH 059/118] fix typo in nominatim docs --- docs/install/nominatim.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install/nominatim.md b/docs/install/nominatim.md index 034864ee..5202861a 100644 --- a/docs/install/nominatim.md +++ b/docs/install/nominatim.md @@ -106,7 +106,7 @@ docker-compose pull See [Requirements](#requirements) about downloading the `.osm.pbf` files ```bash cd docs/docker/nominatim/ -wget https://download.geofabrik.de/europe/italy/nord-ovest-updates/nord-ovest-latest.osm.pbf \ +wget https://download.geofabrik.de/europe/italy/nord-ovest-latest.osm.pbf \ ./nominatim/data/default.osm.pbf ``` From e8f744960b95f4c87f8047b79314939093f86e8f Mon Sep 17 00:00:00 2001 From: sedum Date: Tue, 20 Dec 2022 06:26:37 +0100 Subject: [PATCH 060/118] add missing photon osm_property locality --- components/WhereInput.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/WhereInput.vue b/components/WhereInput.vue index 0caa47c4..1b41a0ee 100644 --- a/components/WhereInput.vue +++ b/components/WhereInput.vue @@ -256,7 +256,7 @@ export default { this.addressList = [] } } else if (this.geocoding_provider_type == "Photon") { - let photon_properties = ['housenumber', 'street', 'district', 'city', 'county', 'state', 'postcode', 'country'] + let photon_properties = ['housenumber', 'street', 'locality', 'district', 'city', 'county', 'state', 'postcode', 'country'] if (ret) { this.addressList = ret.features.map(v => { From cbed0288febff621029d6a71b0c9087c7dad439c Mon Sep 17 00:00:00 2001 From: lesion Date: Fri, 23 Dec 2022 01:08:14 +0100 Subject: [PATCH 061/118] models initialization refactored, better dev experience as backend hmr is working --- CHANGELOG | 4 + package.json | 2 +- server/api/controller/announce.js | 3 +- server/api/controller/ap_user.js | 2 +- server/api/controller/collection.js | 20 +- server/api/controller/event.js | 39 +-- server/api/controller/export.js | 4 +- server/api/controller/instance.js | 5 +- server/api/controller/metrics.js | 2 +- server/api/controller/oauth.js | 5 +- server/api/controller/place.js | 4 +- server/api/controller/plugins.js | 8 +- server/api/controller/resource.js | 4 +- server/api/controller/settings.js | 13 +- server/api/controller/setup.js | 10 +- server/api/controller/tag.js | 47 +-- server/api/controller/user.js | 2 +- server/api/index.js | 424 +++++++++++++------------ server/api/models/announcement.js | 18 +- server/api/models/ap_user.js | 11 +- server/api/models/collection.js | 48 ++- server/api/models/event.js | 195 +++++------- server/api/models/eventnotification.js | 12 +- server/api/models/filter.js | 44 ++- server/api/models/index.js | 80 ++++- server/api/models/instance.js | 17 +- server/api/models/models.js | 2 + server/api/models/notification.js | 13 +- server/api/models/oauth_client.js | 13 +- server/api/models/oauth_code.js | 19 +- server/api/models/oauth_token.js | 19 +- server/api/models/place.js | 12 +- server/api/models/resource.js | 17 +- server/api/models/setting.js | 12 +- server/api/models/tag.js | 12 +- server/api/models/user.js | 84 +++-- server/cli/accounts.js | 2 +- server/federation/helpers.js | 6 +- server/helpers.js | 2 +- server/initialize.server.js | 15 +- server/notifier.js | 41 ++- server/routes.js | 21 +- yarn.lock | 18 +- 43 files changed, 624 insertions(+), 707 deletions(-) create mode 100644 server/api/models/models.js diff --git a/CHANGELOG b/CHANGELOG index cbeb5a5c..5b72fad8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ All notable changes to this project will be documented in this file. +### + + - models initialization refactored, better dev xperience as backend hmr is working + ### 1.6.1 - 15 dec '22 - allow edit tags in admin panel, fix #170 - fix header / fallback image upload, fix #222 diff --git a/package.json b/package.json index 4e8ceceb..461ab87a 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "passport-oauth2-client-password": "^0.1.2", "passport-oauth2-client-public": "^0.0.1", "pg": "^8.8.0", - "sequelize": "^6.27.0", + "sequelize": "^6.28.0", "sequelize-slugify": "^1.6.2", "sharp": "^0.27.2", "sqlite3": "^5.1.4", diff --git a/server/api/controller/announce.js b/server/api/controller/announce.js index 24a56827..b9e9c826 100644 --- a/server/api/controller/announce.js +++ b/server/api/controller/announce.js @@ -1,4 +1,5 @@ -const Announcement = require('../models/announcement') +const { Announcement } = require('../models/models') + const log = require('../../log') const announceController = { diff --git a/server/api/controller/ap_user.js b/server/api/controller/ap_user.js index 4a2e9aa2..7673194c 100644 --- a/server/api/controller/ap_user.js +++ b/server/api/controller/ap_user.js @@ -1,4 +1,4 @@ -const APUser = require('../models/ap_user') +const { APUser } = require('../models/models') const apUserController = { async toggleBlock (req, res) { diff --git a/server/api/controller/collection.js b/server/api/controller/collection.js index 065c85ed..b00493a0 100644 --- a/server/api/controller/collection.js +++ b/server/api/controller/collection.js @@ -1,8 +1,5 @@ -const Collection = require('../models/collection') -const Filter = require('../models/filter') -const Event = require('../models/event') -const Tag = require('../models/tag') -const Place = require('../models/place') +const { Collection, Filter, Event, Tag, Place } = require('../models/models') + const log = require('../../log') const dayjs = require('dayjs') const { col: Col } = require('../../helpers') @@ -114,7 +111,7 @@ const collectionController = { res.json(collection) } catch (e) { log.error(`Create collection failed ${e}`) - res.sendStatus(400) + res.status(400).send(e) } }, @@ -138,15 +135,14 @@ const collectionController = { }, async addFilter (req, res) { - const collectionId = req.body.collectionId - const tags = req.body.tags - const places = req.body.places + const { collectionId, tags, places } = req.body + try { - const filter = await Filter.create({ collectionId, tags, places }) + filter = await Filter.create({ collectionId, tags, places }) return res.json(filter) } catch (e) { log.error(String(e)) - return res.status(500) + return res.sendStatus(400) } }, @@ -170,6 +166,4 @@ const collectionController = { } - - module.exports = collectionController \ No newline at end of file diff --git a/server/api/controller/event.js b/server/api/controller/event.js index 2c801af9..9a16aacf 100644 --- a/server/api/controller/event.js +++ b/server/api/controller/event.js @@ -9,12 +9,10 @@ const Sequelize = require('sequelize') const dayjs = require('dayjs') const helpers = require('../../helpers') const Col = helpers.col -const Event = require('../models/event') -const Resource = require('../models/resource') -const Tag = require('../models/tag') -const Place = require('../models/place') -const Notification = require('../models/notification') -const APUser = require('../models/ap_user') +const notifier = require('../../notifier') + +const { Event, Resource, Tag, Place, Notification, APUser } = require('../models/models') + const exportController = require('./export') const tagController = require('./tag') @@ -155,34 +153,6 @@ const eventController = { }, - async getNotifications(event, action) { - log.debug(`getNotifications ${event.title} ${action}`) - function match(event, filters) { - // matches if no filter specified - if (!filters) { return true } - - // check for visibility - if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) { return false } - - if (!filters.tags && !filters.places) { return true } - if (!filters.tags.length && !filters.places.length) { return true } - if (filters.tags.length) { - const m = intersection(event.tags.map(t => t.tag), filters.tags) - if (m.length > 0) { return true } - } - if (filters.places.length) { - if (filters.places.find(p => p === event.place.name)) { - return true - } - } - } - - const notifications = await Notification.findAll({ where: { action }, include: [Event] }) - - // get notification that matches with selected event - return notifications.filter(notification => match(event, notification.filters)) - }, - async _get(slug) { // retrocompatibility, old events URL does not use slug, use id as fallback const id = Number(slug) || -1 @@ -317,7 +287,6 @@ const eventController = { res.sendStatus(200) // send notification - const notifier = require('../../notifier') notifier.notifyEvent('Create', event.id) } catch (e) { log.error('[EVENT]', e) diff --git a/server/api/controller/export.js b/server/api/controller/export.js index 13ac9b8b..2aa410b1 100644 --- a/server/api/controller/export.js +++ b/server/api/controller/export.js @@ -1,6 +1,4 @@ -const Event = require('../models/event') -const Place = require('../models/place') -const Tag = require('../models/tag') +const { Event, Place, Tag } = require('../models/models') const { htmlToText } = require('html-to-text') const { Op, literal } = require('sequelize') diff --git a/server/api/controller/instance.js b/server/api/controller/instance.js index 2cea4141..47baa2ee 100644 --- a/server/api/controller/instance.js +++ b/server/api/controller/instance.js @@ -1,6 +1,5 @@ -const APUser = require('../models/ap_user') -const Instance = require('../models/instance') -const Resource = require('../models/resource') +const { APUser, Instance, Resource } = require('../models/models') + const Sequelize = require('sequelize') const instancesController = { diff --git a/server/api/controller/metrics.js b/server/api/controller/metrics.js index f4b5ec08..0d4bb287 100644 --- a/server/api/controller/metrics.js +++ b/server/api/controller/metrics.js @@ -1,4 +1,4 @@ -const User = require('../models/user') +const User = require('../models/modles') const metrics = { diff --git a/server/api/controller/oauth.js b/server/api/controller/oauth.js index e7e76ed3..a1a1ef9a 100644 --- a/server/api/controller/oauth.js +++ b/server/api/controller/oauth.js @@ -2,12 +2,9 @@ const bodyParser = require('body-parser') const cookieParser = require('cookie-parser') const session = require('cookie-session') -const OAuthClient = require('../models/oauth_client') -const OAuthToken = require('../models/oauth_token') -const OAuthCode = require('../models/oauth_code') +const { OAuthClient, OAuthToken, OAuthCode, User } = require('../models/models') const helpers = require('../../helpers.js') -const User = require('../models/user') const passport = require('passport') const get = require('lodash/get') diff --git a/server/api/controller/place.js b/server/api/controller/place.js index f84b0d76..8fd1aa3b 100644 --- a/server/api/controller/place.js +++ b/server/api/controller/place.js @@ -1,5 +1,5 @@ -const Place = require('../models/place') -const Event = require('../models/event') +const { Place, Event } = require('../models/models') + const eventController = require('./event') const exportController = require('./export') diff --git a/server/api/controller/plugins.js b/server/api/controller/plugins.js index defcca40..49833366 100644 --- a/server/api/controller/plugins.js +++ b/server/api/controller/plugins.js @@ -2,11 +2,12 @@ const path = require('path') const fs = require('fs') const log = require('../../log') const config = require('../../config') +const settingsController = require('./settings') +const notifier = require('../../notifier') const pluginController = { plugins: [], getAll(_req, res) { - const settingsController = require('./settings') // return plugins and inner settings const plugins = pluginController.plugins.map( ({ configuration }) => { if (settingsController.settings['plugin_' + configuration.name]) { @@ -18,7 +19,6 @@ const pluginController = { }, togglePlugin(req, res) { - const settingsController = require('./settings') const pluginName = req.params.plugin const pluginSettings = settingsController.settings['plugin_' + pluginName] if (!pluginSettings) { return res.sendStatus(404) } @@ -33,7 +33,6 @@ const pluginController = { }, unloadPlugin(pluginName) { - const settingsController = require('./settings') const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName) const settings = settingsController.settings['plugin_' + pluginName] if (!plugin) { @@ -59,14 +58,12 @@ const pluginController = { }, loadPlugin(pluginName) { - const settingsController = require('./settings') const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName) const settings = settingsController.settings['plugin_' + pluginName] if (!plugin) { log.warn(`Plugin ${pluginName} not found`) return } - const notifier = require('../../notifier') log.info('Load plugin ' + pluginName) if (typeof plugin.onEventCreate === 'function') { notifier.emitter.on('Create', plugin.onEventCreate) @@ -88,7 +85,6 @@ const pluginController = { }, _load() { - const settingsController = require('./settings') // load custom plugins const plugins_path = config.plugins_path || path.resolve(process.env.cwd || '', 'plugins') log.info(`Loading plugin ${plugins_path}`) diff --git a/server/api/controller/resource.js b/server/api/controller/resource.js index 2d854de2..9d151094 100644 --- a/server/api/controller/resource.js +++ b/server/api/controller/resource.js @@ -1,6 +1,4 @@ -const Resource = require('../models/resource') -const APUser = require('../models/ap_user') -const Event = require('../models/event') +const { Resource, APUser, Event } = require('../models/models') const get = require('lodash/get') const resourceController = { diff --git a/server/api/controller/settings.js b/server/api/controller/settings.js index b3d1dfad..0645bb26 100644 --- a/server/api/controller/settings.js +++ b/server/api/controller/settings.js @@ -1,6 +1,5 @@ const path = require('path') const URL = require('url') -const fs = require('fs') const crypto = require('crypto') const { promisify } = require('util') const sharp = require('sharp') @@ -9,7 +8,7 @@ const generateKeyPair = promisify(crypto.generateKeyPair) const log = require('../../log') // const locales = require('../../../locales/index') const escape = require('lodash/escape') -const pluginController = require('./plugins') +const DB = require('../models/models') let defaultHostname try { @@ -30,7 +29,7 @@ const defaultSettings = { allow_multidate_event: true, allow_recurrent_event: false, recurrent_event_visible: false, - allow_geolocation: true, + allow_geolocation: false, geocoding_provider_type: 'Nominatim', geocoding_provider: 'https://nominatim.openstreetmap.org/search', geocoding_countrycodes: [], @@ -74,8 +73,7 @@ const settingsController = { // 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() + const settings = await DB.Setting.findAll() settingsController.settings = defaultSettings settings.forEach(s => { if (s.is_secret) { @@ -117,15 +115,14 @@ const settingsController = { // } // }) // } - + const pluginController = require('./plugins') pluginController._load() }, async set (key, value, is_secret = false) { - const Setting = require('../models/setting') log.info(`SET ${key} ${is_secret ? '*****' : value}`) try { - const [setting, created] = await Setting.findOrCreate({ + const [setting, created] = await DB.Setting.findOrCreate({ where: { key }, defaults: { value, is_secret } }) diff --git a/server/api/controller/setup.js b/server/api/controller/setup.js index 541c5a27..90be7be3 100644 --- a/server/api/controller/setup.js +++ b/server/api/controller/setup.js @@ -7,6 +7,8 @@ const settingsController = require('./settings') const path = require('path') const escape = require('lodash/escape') +const DB = require('../models/models') + const setupController = { async _setupDb (dbConf) { @@ -23,7 +25,10 @@ const setupController = { // try to connect dbConf.logging = false - await db.connect(dbConf) + db.connect(dbConf) + db.loadModels() + db.associates() + await db.sequelize.authenticate() // is empty ? const isEmpty = await db.isEmpty() @@ -69,8 +74,7 @@ const setupController = { // create admin const password = helpers.randomString() const email = `admin` - const User = require('../models/user') - await User.create({ + await DB.User.create({ email, password, is_admin: true, diff --git a/server/api/controller/tag.js b/server/api/controller/tag.js index d581179b..66d30320 100644 --- a/server/api/controller/tag.js +++ b/server/api/controller/tag.js @@ -1,5 +1,4 @@ -const Tag = require('../models/tag') -const Event = require('../models/event') +const { Tag, Event } = require('../models/models') const uniq = require('lodash/uniq') const log = require('../../log') @@ -82,29 +81,35 @@ module.exports = { return res.json(tags.map(t => t.tag)) }, - async updateTag (req, res) { - const tag = await Tag.findByPk(req.body.tag) - await tag.update(req.body) - res.json(place) - }, + // async updateTag (req, res) { + // const tag = await Tag.findByPk(req.body.tag) + // await tag.update(req.body) + // res.json(place) + // }, async updateTag (req, res) { - const oldtag = await Tag.findByPk(req.body.tag) - const newtag = await Tag.findByPk(req.body.newTag) + try { + const oldtag = await Tag.findByPk(req.body.tag) + const newtag = await Tag.findByPk(req.body.newTag) - // if the new tag does not exists, just rename the old one - if (!newtag) { - oldtag.tag = req.body.newTag - await oldtag.update({ tag: req.body.newTag }) - } else { - // in case it exists: - // - search for events with old tag - const events = await oldtag.getEvents() - // - substitute it with the new one - await oldtag.removeEvents(events) - await newtag.addEvents(events) + // if the new tag does not exists, just rename the old one + if (!newtag) { + log.info(`Rename tag ${oldtag.tag} to ${req.body.newTag}`) + await Tag.update({ tag: req.body.newTag }, { where: { tag: req.body.tag }, raw: true }) + + } else { + // in case it exists: + // - search for events with old tag + const events = await oldtag.getEvents() + // - substitute it with the new one + await oldtag.removeEvents(events) + await newtag.addEvents(events) + } + res.sendStatus(200) + } catch (e) { + console.error(e) + res.sendStatus(400) } - res.sendStatus(200) }, async remove (req, res) { diff --git a/server/api/controller/user.js b/server/api/controller/user.js index c1e482ee..4fa8c900 100644 --- a/server/api/controller/user.js +++ b/server/api/controller/user.js @@ -2,7 +2,7 @@ const crypto = require('crypto') const { Op } = require('sequelize') const config = require('../../config') const mail = require('../mail') -const User = require('../models/user') +const { User } = require('../models/models') const settingsController = require('./settings') const log = require('../../log') const linkify = require('linkifyjs') diff --git a/server/api/index.js b/server/api/index.js index 4fbed0a9..8d1ccbc9 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -5,219 +5,223 @@ const cors = require('cors')() const config = require('../config') const log = require('../log') -const api = express.Router() -api.use(express.urlencoded({ extended: false })) -api.use(express.json()) +const collectionController = require('./controller/collection') +const setupController = require('./controller/setup') +const settingsController = require('./controller/settings') +const eventController = require('./controller/event') +const placeController = require('./controller/place') +const tagController = require('./controller/tag') +const exportController = require('./controller/export') +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 pluginController = require('./controller/plugins') +const helpers = require('../helpers') +const storage = require('./storage') -if (config.status !== 'READY') { +module.exports = () => { - const setupController = require('./controller/setup') - const settingsController = require('./controller/settings') - api.post('/settings', settingsController.setRequest) - api.post('/setup/db', setupController.setupDb) - api.post('/setup/restart', setupController.restart) - api.post('/settings/smtp', settingsController.testSMTP) - -} else { - - const { isAuth, isAdmin } = require('./auth') - const eventController = require('./controller/event') - const placeController = require('./controller/place') - const tagController = require('./controller/tag') - const settingsController = require('./controller/settings') - const exportController = require('./controller/export') - 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 collectionController = require('./controller/collection') - const pluginController = require('./controller/plugins') - 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 + const api = express.Router() + api.use(express.urlencoded({ extended: false })) + api.use(express.json()) + + + if (config.status !== 'READY') { + + api.post('/settings', settingsController.setRequest) + api.post('/setup/db', setupController.setupDb) + api.post('/setup/restart', setupController.restart) + api.post('/settings/smtp', settingsController.testSMTP) + + } else { + + const { isAuth, isAdmin } = require('./auth') + 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', isAuth, userController.remove) + + // get all users + api.get('/users', isAdmin, userController.getAll) + + /** + * Get events + * @category Event + * @name /api/events + * @type GET + * @param {integer} [start] - start timestamp (default: now) + * @param {integer} [end] - end timestamp (optional) + * @param {array} [tags] - List of tags + * @param {array} [places] - List of places id + * @param {integer} [max] - Limit events + * @param {boolean} [show_recurrent] - Show also recurrent events (default: as choosen in admin settings) + * @param {integer} [page] - Pagination + * @param {boolean} [older] - select <= start instead of >= + * @example ***Example*** + * [https://demo.gancio.org/api/events](https://demo.gancio.org/api/events) + * [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42) + */ + + api.get('/events', cors, eventController.select) + + /** + * Add a new event + * @category Event + * @name /api/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 {float} [place_latitude] - the latitude of the place + * @param {float} [place_longitude] - the longitude 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 {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', eventController.isAnonEventAllowed, upload.single('image'), eventController.add) + + api.get('/event/search', eventController.search) + + 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.searchMeta) + + // add event notification TODO + api.post('/event/notification', eventController.addNotification) + api.delete('/event/notification/:code', eventController.delNotification) + + api.post('/settings', isAdmin, settingsController.setRequest) + api.get('/settings', isAdmin, settingsController.getAll) + api.post('/settings/logo', isAdmin, multer({ dest: config.upload_path }).single('logo'), settingsController.setLogo) + api.post('/settings/fallbackImage', isAdmin, multer({ dest: config.upload_path }).single('fallbackImage'), settingsController.setFallbackImage) + api.post('/settings/headerImage', isAdmin, multer({ dest: config.upload_path }).single('headerImage'), settingsController.setHeaderImage) + api.post('/settings/smtp', isAdmin, settingsController.testSMTP) + api.get('/settings/smtp', isAdmin, settingsController.getSMTPSettings) + + // get unconfirmed events + api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed) + + // [un]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) + + + // - PLACES + api.get('/places', isAdmin, placeController.getAll) + api.get('/place/:placeName', cors, placeController.getEvents) + api.get('/place', cors, placeController.search) + api.get('/placeOSM/Nominatim/:place_details', cors, placeController._nominatim) + api.get('/placeOSM/Photon/:place_details', cors, placeController._photon) + api.put('/place', isAdmin, placeController.updatePlace) + + // - TAGS + api.get('/tags', isAdmin, tagController.getAll) + api.get('/tag', cors, tagController.search) + api.get('/tag/:tag', cors, tagController.getEvents) + api.delete('/tag/:tag', isAdmin, tagController.remove) + api.put('/tag', isAdmin, tagController.updateTag) + + + // - FEDIVERSE INSTANCES, MODERATION, RESOURCES + 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) + + // - COLLECTIONS + api.get('/collections/:name', cors, collectionController.getEvents) + api.get('/collections', collectionController.getAll) + api.post('/collections', isAdmin, collectionController.add) + api.delete('/collection/:id', isAdmin, collectionController.remove) + api.get('/filter/:collection_id', isAdmin, collectionController.getFilters) + api.post('/filter', isAdmin, collectionController.addFilter) + api.delete('/filter/:id', isAdmin, collectionController.removeFilter) + + // - PLUGINS + api.get('/plugins', isAdmin, pluginController.getAll) + api.put('/plugin/:plugin', isAdmin, pluginController.togglePlugin) + + // OAUTH + api.get('/clients', isAuth, oauthController.getClients) + api.get('/client/:client_id', isAuth, oauthController.getClient) + api.post('/client', oauthController.createClient) } - ``` - */ - api.get('/ping', (_req, res) => res.sendStatus(200)) - api.get('/user', isAuth, (req, res) => res.json(req.user)) + + api.use((_req, res) => res.sendStatus(404)) + + // Handle 500 + api.use((error, _req, res, _next) => { + log.error('[API ERROR]', error) + res.status(500).send('500: Internal Server Error') + }) - - 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', isAuth, userController.remove) - - // get all users - api.get('/users', isAdmin, userController.getAll) - - /** - * Get events - * @category Event - * @name /api/events - * @type GET - * @param {integer} [start] - start timestamp (default: now) - * @param {integer} [end] - end timestamp (optional) - * @param {array} [tags] - List of tags - * @param {array} [places] - List of places id - * @param {integer} [max] - Limit events - * @param {boolean} [show_recurrent] - Show also recurrent events (default: as choosen in admin settings) - * @param {integer} [page] - Pagination - * @param {boolean} [older] - select <= start instead of >= - * @example ***Example*** - * [https://demo.gancio.org/api/events](https://demo.gancio.org/api/events) - * [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42) - */ - - api.get('/events', cors, eventController.select) - - /** - * Add a new event - * @category Event - * @name /api/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 {float} [place_latitude] - the latitude of the place - * @param {float} [place_longitude] - the longitude 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 {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', eventController.isAnonEventAllowed, upload.single('image'), eventController.add) - - api.get('/event/search', eventController.search) - - 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.searchMeta) - - // add event notification TODO - api.post('/event/notification', eventController.addNotification) - api.delete('/event/notification/:code', eventController.delNotification) - - api.post('/settings', isAdmin, settingsController.setRequest) - api.get('/settings', isAdmin, settingsController.getAll) - api.post('/settings/logo', isAdmin, multer({ dest: config.upload_path }).single('logo'), settingsController.setLogo) - api.post('/settings/fallbackImage', isAdmin, multer({ dest: config.upload_path }).single('fallbackImage'), settingsController.setFallbackImage) - api.post('/settings/headerImage', isAdmin, multer({ dest: config.upload_path }).single('headerImage'), settingsController.setHeaderImage) - api.post('/settings/smtp', isAdmin, settingsController.testSMTP) - api.get('/settings/smtp', isAdmin, settingsController.getSMTPSettings) - - // get unconfirmed events - api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed) - - // [un]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) - - - // - PLACES - api.get('/places', isAdmin, placeController.getAll) - api.get('/place/:placeName', cors, placeController.getEvents) - api.get('/place', cors, placeController.search) - api.get('/placeOSM/Nominatim/:place_details', cors, placeController._nominatim) - api.get('/placeOSM/Photon/:place_details', cors, placeController._photon) - api.put('/place', isAdmin, placeController.updatePlace) - - // - TAGS - api.get('/tags', isAdmin, tagController.getAll) - api.get('/tag', cors, tagController.search) - api.get('/tag/:tag', cors, tagController.getEvents) - api.delete('/tag/:tag', isAdmin, tagController.remove) - api.put('/tag', isAdmin, tagController.updateTag) - - - // - FEDIVERSE INSTANCES, MODERATION, RESOURCES - 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) - - // - COLLECTIONS - api.get('/collections/:name', cors, collectionController.getEvents) - api.get('/collections', collectionController.getAll) - api.post('/collections', isAdmin, collectionController.add) - api.delete('/collection/:id', isAdmin, collectionController.remove) - api.get('/filter/:collection_id', isAdmin, collectionController.getFilters) - api.post('/filter', isAdmin, collectionController.addFilter) - api.delete('/filter/:id', isAdmin, collectionController.removeFilter) - - // - PLUGINS - api.get('/plugins', isAdmin, pluginController.getAll) - api.put('/plugin/:plugin', isAdmin, pluginController.togglePlugin) - - // OAUTH - api.get('/clients', isAuth, oauthController.getClients) - api.get('/client/:client_id', isAuth, oauthController.getClient) - api.post('/client', oauthController.createClient) + return api } - -api.use((_req, res) => res.sendStatus(404)) - -// Handle 500 -api.use((error, _req, res, _next) => { - log.error('[API ERROR]', error) - res.status(500).send('500: Internal Server Error') -}) - -module.exports = api diff --git a/server/api/models/announcement.js b/server/api/models/announcement.js index 0b0926c5..6b410793 100644 --- a/server/api/models/announcement.js +++ b/server/api/models/announcement.js @@ -1,12 +1,6 @@ -const sequelize = require('./index').sequelize -const { Model, DataTypes } = require('sequelize') - -class Announcement extends Model {} - -Announcement.init({ - title: DataTypes.STRING, - announcement: DataTypes.STRING, - visible: DataTypes.BOOLEAN -}, { sequelize, modelName: 'announcement' }) - -module.exports = Announcement +module.exports = (sequelize, DataTypes) => + sequelize.define('announcement', { + title: DataTypes.STRING, + announcement: DataTypes.STRING, + visible: DataTypes.BOOLEAN + }) diff --git a/server/api/models/ap_user.js b/server/api/models/ap_user.js index 20e80a55..c2c1d8f7 100644 --- a/server/api/models/ap_user.js +++ b/server/api/models/ap_user.js @@ -1,9 +1,6 @@ -const sequelize = require('./index').sequelize -const { Model, DataTypes } = require('sequelize') -class APUser extends Model {} - -APUser.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('ap_user', { ap_id: { type: DataTypes.STRING, primaryKey: true @@ -11,6 +8,4 @@ APUser.init({ follower: DataTypes.BOOLEAN, blocked: DataTypes.BOOLEAN, object: DataTypes.JSON -}, { sequelize, modelName: 'ap_user' }) - -module.exports = APUser +}) diff --git a/server/api/models/collection.js b/server/api/models/collection.js index 4df48141..d07cee69 100644 --- a/server/api/models/collection.js +++ b/server/api/models/collection.js @@ -1,28 +1,20 @@ -const { Model, DataTypes } = require('sequelize') -const sequelize = require('./index').sequelize - -class Collection extends Model {} - -// TODO: slugify! -Collection.init({ - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - name: { - type: DataTypes.STRING, - unique: true, - index: true, - allowNull: false - }, - isActor: { - type: DataTypes.BOOLEAN - }, - isTop: { - type: DataTypes.BOOLEAN - } -}, { sequelize, modelName: 'collection', timestamps: false }) - - -module.exports = Collection +module.exports = (sequelize, DataTypes) => + sequelize.define('collection', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: DataTypes.STRING, + unique: true, + index: true, + allowNull: false + }, + isActor: { + type: DataTypes.BOOLEAN + }, + isTop: { + type: DataTypes.BOOLEAN + } + }, { timestamps: false }) diff --git a/server/api/models/event.js b/server/api/models/event.js index 75d2067f..c0e6c7d5 100644 --- a/server/api/models/event.js +++ b/server/api/models/event.js @@ -1,18 +1,5 @@ const config = require('../../config') const { htmlToText } = require('html-to-text') - -const { Model, DataTypes } = require('sequelize') -const SequelizeSlugify = require('sequelize-slugify') - -const sequelize = require('./index').sequelize - -const Resource = require('./resource') -const Notification = require('./notification') -const EventNotification = require('./eventnotification') -const Place = require('./place') -const User = require('./user') -const Tag = require('./tag') - const dayjs = require('dayjs') const timezone = require('dayjs/plugin/timezone') const utc = require('dayjs/plugin/utc') @@ -20,108 +7,88 @@ const utc = require('dayjs/plugin/utc') dayjs.extend(utc) dayjs.extend(timezone) -class Event extends Model {} - -Event.init({ - id: { - allowNull: false, - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - title: DataTypes.STRING, - slug: { - type: DataTypes.STRING, - index: true, - unique: true - }, - description: DataTypes.TEXT, - multidate: DataTypes.BOOLEAN, - start_datetime: { - type: DataTypes.INTEGER, - index: true - }, - end_datetime: { - type: DataTypes.INTEGER, - index: true - }, - image_path: DataTypes.STRING, - media: DataTypes.JSON, - is_visible: DataTypes.BOOLEAN, - recurrent: DataTypes.JSON, - likes: { type: DataTypes.JSON, defaultValue: [] }, - boost: { type: DataTypes.JSON, defaultValue: [] } -}, { sequelize, modelName: 'event' }) - -Event.belongsTo(Place) -Place.hasMany(Event) - -Event.belongsTo(User) -User.hasMany(Event) - -Event.belongsToMany(Tag, { through: 'event_tags' }) -Tag.belongsToMany(Event, { through: 'event_tags' }) - -Event.belongsToMany(Notification, { through: EventNotification }) -Notification.belongsToMany(Event, { through: EventNotification }) - -Event.hasMany(Resource) -Resource.belongsTo(Event) - -Event.hasMany(Event, { as: 'child', foreignKey: 'parentId' }) -Event.belongsTo(Event, { as: 'parent' }) - -SequelizeSlugify.slugifyModel(Event, { source: ['title'], overwrite: false }) - -Event.prototype.toAP = function (username, locale, to = []) { - const tags = this.tags && this.tags.map(t => t.tag.replace(/[ #]/g, '_')) - const plainDescription = htmlToText(this.description && this.description.replace('\n', '').slice(0, 1000)) - const content = ` +// class Event extends Model {} +module.exports = (sequelize, DataTypes) => { + const Event = sequelize.define('event', { + id: { + allowNull: false, + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + title: DataTypes.STRING, + slug: { + type: DataTypes.STRING, + index: true, + unique: true + }, + description: DataTypes.TEXT, + multidate: DataTypes.BOOLEAN, + start_datetime: { + type: DataTypes.INTEGER, + index: true + }, + end_datetime: { + type: DataTypes.INTEGER, + index: true + }, + image_path: DataTypes.STRING, + media: DataTypes.JSON, + is_visible: DataTypes.BOOLEAN, + recurrent: DataTypes.JSON, + likes: { type: DataTypes.JSON, defaultValue: [] }, + boost: { type: DataTypes.JSON, defaultValue: [] } + }) + + Event.prototype.toAP = function (username, locale, to = []) { + const tags = this.tags && this.tags.map(t => t.tag.replace(/[ #]/g, '_')) + const plainDescription = htmlToText(this.description && this.description.replace('\n', '').slice(0, 1000)) + const content = ` 📍 ${this.place && this.place.name} 📅 ${dayjs.unix(this.start_datetime).tz().locale(locale).format('dddd, D MMMM (HH:mm)')} - + ${plainDescription} - ` - - const attachment = [] - if (this.media && this.media.length) { - attachment.push({ - type: 'Document', - mediaType: 'image/jpeg', - url: `${config.baseurl}/media/${this.media[0].url}`, - name: this.media[0].name || this.title || '', - blurHash: null, - focalPoint: this.media[0].focalPoint || [0, 0] - }) + ` + + const attachment = [] + if (this.media && this.media.length) { + attachment.push({ + type: 'Document', + mediaType: 'image/jpeg', + url: `${config.baseurl}/media/${this.media[0].url}`, + name: this.media[0].name || this.title || '', + blurHash: null, + focalPoint: this.media[0].focalPoint || [0, 0] + }) + } + + + return { + id: `${config.baseurl}/federation/m/${this.id}`, + name: this.title, + url: `${config.baseurl}/event/${this.slug || this.id}`, + type: 'Event', + startTime: dayjs.unix(this.start_datetime).tz().locale(locale).format(), + endTime: this.end_datetime ? dayjs.unix(this.end_datetime).tz().locale(locale).format() : null, + location: { + name: this.place.name, + address: this.place.address, + latitude: this.place.latitude, + longitude: this.place.longitude + }, + attachment, + tag: tags && tags.map(tag => ({ + type: 'Hashtag', + name: '#' + tag, + href: `${config.baseurl}/tag/${tag}` + })), + published: dayjs(this.createdAt).utc().format(), + attributedTo: `${config.baseurl}/federation/u/${username}`, + to: ['https://www.w3.org/ns/activitystreams#Public'], + cc: [`${config.baseurl}/federation/u/${username}/followers`], + content, + summary: content + } } - - - return { - id: `${config.baseurl}/federation/m/${this.id}`, - name: this.title, - url: `${config.baseurl}/event/${this.slug || this.id}`, - type: 'Event', - startTime: dayjs.unix(this.start_datetime).tz().locale(locale).format(), - endTime: this.end_datetime ? dayjs.unix(this.end_datetime).tz().locale(locale).format() : null, - location: { - name: this.place.name, - address: this.place.address, - latitude: this.place.latitude, - longitude: this.place.longitude - }, - attachment, - tag: tags && tags.map(tag => ({ - type: 'Hashtag', - name: '#' + tag, - href: `${config.baseurl}/tag/${tag}` - })), - published: dayjs(this.createdAt).utc().format(), - attributedTo: `${config.baseurl}/federation/u/${username}`, - to: ['https://www.w3.org/ns/activitystreams#Public'], - cc: [`${config.baseurl}/federation/u/${username}/followers`], - content, - summary: content - } -} - -module.exports = Event + return Event +} \ No newline at end of file diff --git a/server/api/models/eventnotification.js b/server/api/models/eventnotification.js index 662f05e9..ad8dfac0 100644 --- a/server/api/models/eventnotification.js +++ b/server/api/models/eventnotification.js @@ -1,15 +1,9 @@ -const sequelize = require('./index').sequelize -const { Model, DataTypes } = require('sequelize') - -class EventNotification extends Model {} - -EventNotification.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('event_notification', { status: { type: DataTypes.ENUM, values: ['new', 'sent', 'error', 'sending'], defaultValue: 'new', index: true } -}, { sequelize, modelName: 'event_notification' }) - -module.exports = EventNotification +}) \ No newline at end of file diff --git a/server/api/models/filter.js b/server/api/models/filter.js index 1a6cbe8a..a1077035 100644 --- a/server/api/models/filter.js +++ b/server/api/models/filter.js @@ -1,24 +1,20 @@ -const { Model, DataTypes } = require('sequelize') -const Collection = require('./collection') -const sequelize = require('./index').sequelize - -class Filter extends Model {} - -Filter.init({ - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - }, - tags: { - type: DataTypes.JSON, - }, - places: { - type: DataTypes.JSON, - } -}, { sequelize, modelName: 'filter', timestamps: false }) - -Filter.belongsTo(Collection) -Collection.hasMany(Filter) - -module.exports = Filter +module.exports = (sequelize, DataTypes) => + sequelize.define('filter', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + tags: { + type: DataTypes.JSON, + }, + places: { + type: DataTypes.JSON, + } + }, { + indexes: [ + { fields: ['collectionId', 'tags', 'places'], unique: true } + ], + timestamps: false + }) \ No newline at end of file diff --git a/server/api/models/index.js b/server/api/models/index.js index 2174f1dc..be5308bb 100644 --- a/server/api/models/index.js +++ b/server/api/models/index.js @@ -4,10 +4,78 @@ const Umzug = require('umzug') const path = require('path') const config = require('../../config') const log = require('../../log') -const settingsController = require('../controller/settings') +const SequelizeSlugify = require('sequelize-slugify') +const DB = require('./models') + +const models = { + Announcement: require('./announcement'), + APUser: require('./ap_user'), + Collection: require('./collection'), + Event: require('./event'), + EventNotification: require('./eventnotification'), + Filter: require('./filter'), + Instance: require('./instance'), + Notification: require('./notification'), + OAuthClient: require('./oauth_client'), + OAuthCode: require('./oauth_code'), + OAuthToken: require('./oauth_token'), + Place: require('./place'), + Resource: require('./resource'), + Setting: require('./setting'), + Tag: require('./tag'), + User: require('./user'), +} const db = { sequelize: null, + loadModels () { + + for (const modelName in models) { + const m = models[modelName](db.sequelize, Sequelize.DataTypes) + DB[modelName] = m + } + + }, + associates () { + const { Filter, Collection, APUser, Instance, User, Event, EventNotification, Tag, + OAuthCode, OAuthClient, OAuthToken, Resource, Place, Notification } = DB + + Filter.belongsTo(Collection) + Collection.hasMany(Filter) + + Instance.hasMany(APUser) + APUser.belongsTo(Instance) + + OAuthCode.belongsTo(User) + OAuthCode.belongsTo(OAuthClient, { as: 'client' }) + + OAuthToken.belongsTo(User) + OAuthToken.belongsTo(OAuthClient, { as: 'client' }) + + APUser.hasMany(Resource) + Resource.belongsTo(APUser) + + Event.belongsTo(Place) + Place.hasMany(Event) + + Event.belongsTo(User) + User.hasMany(Event) + + Event.belongsToMany(Tag, { through: 'event_tags' }) + Tag.belongsToMany(Event, { through: 'event_tags' }) + + Event.belongsToMany(Notification, { through: EventNotification }) + Notification.belongsToMany(Event, { through: EventNotification }) + + Event.hasMany(Resource) + Resource.belongsTo(Event) + + Event.hasMany(Event, { as: 'child', foreignKey: 'parentId' }) + Event.belongsTo(Event, { as: 'parent' }) + + SequelizeSlugify.slugifyModel(Event, { source: ['title'], overwrite: false }) + + }, close() { if (db.sequelize) { return db.sequelize.close() @@ -28,7 +96,6 @@ const db = { } } db.sequelize = new Sequelize(dbConf) - return db.sequelize.authenticate() }, async isEmpty() { try { @@ -57,13 +124,12 @@ const db = { }) return umzug.up() }, - async initialize() { + initialize() { if (config.status === 'CONFIGURED') { try { - await db.connect() - log.debug('Running migrations') - await db.runMigrations() - return settingsController.load() + db.connect() + db.loadModels() + db.associates() } catch (e) { log.warn(` ⚠️ Cannot connect to db, check your configuration => ${e}`) process.exit(1) diff --git a/server/api/models/instance.js b/server/api/models/instance.js index a9f8e96b..3d15e82f 100644 --- a/server/api/models/instance.js +++ b/server/api/models/instance.js @@ -1,11 +1,5 @@ - -const sequelize = require('./index').sequelize -const { Model, DataTypes } = require('sequelize') -const APUser = require('./ap_user') - -class Instance extends Model {} - -Instance.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('instance', { domain: { primaryKey: true, allowNull: false, @@ -14,9 +8,4 @@ Instance.init({ name: DataTypes.STRING, blocked: DataTypes.BOOLEAN, data: DataTypes.JSON -}, { sequelize, modelName: 'instance' }) - -Instance.hasMany(APUser) -APUser.belongsTo(Instance) - -module.exports = Instance +}) \ No newline at end of file diff --git a/server/api/models/models.js b/server/api/models/models.js new file mode 100644 index 00000000..0b46a05d --- /dev/null +++ b/server/api/models/models.js @@ -0,0 +1,2 @@ +// export default models +module.exports = {} \ No newline at end of file diff --git a/server/api/models/notification.js b/server/api/models/notification.js index 19879a77..8d36e89e 100644 --- a/server/api/models/notification.js +++ b/server/api/models/notification.js @@ -1,10 +1,5 @@ - -const sequelize = require('./index').sequelize -const { Model, DataTypes } = require('sequelize') - -class Notification extends Model {} - -Notification.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('notification', { filters: DataTypes.JSON, email: DataTypes.STRING, remove_code: DataTypes.STRING, @@ -24,6 +19,4 @@ Notification.init({ unique: true, fields: ['action', 'type'] }] -}) - -module.exports = Notification +}) \ No newline at end of file diff --git a/server/api/models/oauth_client.js b/server/api/models/oauth_client.js index 103aaf59..e30e05ab 100644 --- a/server/api/models/oauth_client.js +++ b/server/api/models/oauth_client.js @@ -1,10 +1,5 @@ - -const sequelize = require('./index').sequelize -const { Model, DataTypes } = require('sequelize') - -class OAuthClient extends Model {} - -OAuthClient.init({ +module.exports = (sequelize, DataTypes) => +sequelize.define('oauth_client', { id: { type: DataTypes.STRING, primaryKey: true, @@ -15,6 +10,4 @@ OAuthClient.init({ scopes: DataTypes.STRING, redirectUris: DataTypes.STRING, website: DataTypes.STRING -}, { sequelize, modelName: 'oauth_client' }) - -module.exports = OAuthClient +}) \ No newline at end of file diff --git a/server/api/models/oauth_code.js b/server/api/models/oauth_code.js index 45b98fe5..ba82de74 100644 --- a/server/api/models/oauth_code.js +++ b/server/api/models/oauth_code.js @@ -1,13 +1,5 @@ - -const sequelize = require('./index').sequelize -const { Model, DataTypes } = require('sequelize') - -const User = require('./user') -const OAuthClient = require('./oauth_client') - -class OAuthCode extends Model {} - -OAuthCode.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('oauth_code', { authorizationCode: { type: DataTypes.STRING, primaryKey: true @@ -15,9 +7,4 @@ OAuthCode.init({ expiresAt: DataTypes.DATE, scope: DataTypes.STRING, redirect_uri: DataTypes.STRING -}, { sequelize, modelName: 'oauth_code' }) - -OAuthCode.belongsTo(User) -OAuthCode.belongsTo(OAuthClient, { as: 'client' }) - -module.exports = OAuthCode +}) \ No newline at end of file diff --git a/server/api/models/oauth_token.js b/server/api/models/oauth_token.js index 2530bf1c..984ece06 100644 --- a/server/api/models/oauth_token.js +++ b/server/api/models/oauth_token.js @@ -1,13 +1,5 @@ - -const sequelize = require('./index').sequelize -const { Model, DataTypes } = require('sequelize') - -const User = require('./user') -const OAuthClient = require('./oauth_client') - -class OAuthToken extends Model {} - -OAuthToken.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('oauth_token', { accessToken: { type: DataTypes.STRING, allowNull: false, @@ -27,9 +19,4 @@ OAuthToken.init({ } }, scope: DataTypes.STRING -}, { sequelize, modelName: 'oauth_token' }) - -OAuthToken.belongsTo(User) -OAuthToken.belongsTo(OAuthClient, { as: 'client' }) - -module.exports = OAuthToken +}) \ No newline at end of file diff --git a/server/api/models/place.js b/server/api/models/place.js index 0294599b..734824d5 100644 --- a/server/api/models/place.js +++ b/server/api/models/place.js @@ -1,9 +1,5 @@ -const { Model, DataTypes } = require('sequelize') -const sequelize = require('./index').sequelize - -class Place extends Model {} - -Place.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('place', { name: { type: DataTypes.STRING, unique: true, @@ -13,6 +9,4 @@ Place.init({ address: DataTypes.STRING, latitude: DataTypes.FLOAT, longitude: DataTypes.FLOAT, -}, { sequelize, modelName: 'place' }) - -module.exports = Place +}) \ No newline at end of file diff --git a/server/api/models/resource.js b/server/api/models/resource.js index 60d65e09..1774287c 100644 --- a/server/api/models/resource.js +++ b/server/api/models/resource.js @@ -1,11 +1,5 @@ -const { Model, DataTypes } = require('sequelize') -const sequelize = require('./index').sequelize - -const APUser = require('./ap_user') - -class Resource extends Model {} - -Resource.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('resource', { activitypub_id: { type: DataTypes.STRING, index: true, @@ -13,9 +7,4 @@ Resource.init({ }, hidden: DataTypes.BOOLEAN, data: DataTypes.JSON -}, { sequelize, modelName: 'resource' }) - -APUser.hasMany(Resource) -Resource.belongsTo(APUser) - -module.exports = Resource +}) \ No newline at end of file diff --git a/server/api/models/setting.js b/server/api/models/setting.js index 5d20fa56..cd73feba 100644 --- a/server/api/models/setting.js +++ b/server/api/models/setting.js @@ -1,9 +1,5 @@ -const { Model, DataTypes } = require('sequelize') -const sequelize = require('./index').sequelize - -class Setting extends Model {} - -Setting.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('setting', { key: { type: DataTypes.STRING, primaryKey: true, @@ -12,6 +8,4 @@ Setting.init({ }, value: DataTypes.JSON, is_secret: DataTypes.BOOLEAN -}, { sequelize, modelName: 'setting' }) - -module.exports = Setting +}) diff --git a/server/api/models/tag.js b/server/api/models/tag.js index 5dd9e300..dfab832b 100644 --- a/server/api/models/tag.js +++ b/server/api/models/tag.js @@ -1,15 +1,9 @@ -const { Model, DataTypes } = require('sequelize') -const sequelize = require('./index').sequelize - -class Tag extends Model {} - -Tag.init({ +module.exports = (sequelize, DataTypes) => + sequelize.define('tag', { tag: { type: DataTypes.STRING, allowNull: false, index: true, primaryKey: true } -}, { sequelize, modelName: 'tag' }) - -module.exports = Tag +}) \ No newline at end of file diff --git a/server/api/models/user.js b/server/api/models/user.js index 7b0b0f6f..68fd5bd0 100644 --- a/server/api/models/user.js +++ b/server/api/models/user.js @@ -1,53 +1,49 @@ const bcrypt = require('bcryptjs') -const { Model, DataTypes } = require('sequelize') -const sequelize = require('./index').sequelize -class User extends Model {} - -User.init({ - settings: { - type: DataTypes.JSON, - defaultValue: [] - }, - email: { - type: DataTypes.STRING, - unique: { msg: 'error.email_taken' }, - validate: { - notEmpty: true +module.exports = (sequelize, DataTypes) => { + const User = sequelize.define('user', { + settings: { + type: DataTypes.JSON, + defaultValue: [] }, - index: true, - allowNull: false - }, - description: DataTypes.TEXT, - password: DataTypes.STRING, - recover_code: DataTypes.STRING, - is_admin: DataTypes.BOOLEAN, - is_active: DataTypes.BOOLEAN -}, { - sequelize, - modelName: 'user', - scopes: { - withoutPassword: { - attributes: { exclude: ['password', 'recover_code'] } + email: { + type: DataTypes.STRING, + unique: { msg: 'error.email_taken' }, + validate: { + notEmpty: true + }, + index: true, + allowNull: false }, - withRecover: { - attributes: { exclude: ['password'] } + description: DataTypes.TEXT, + password: DataTypes.STRING, + recover_code: DataTypes.STRING, + is_admin: DataTypes.BOOLEAN, + is_active: DataTypes.BOOLEAN + }, { + scopes: { + withoutPassword: { + attributes: { exclude: ['password', 'recover_code'] } + }, + withRecover: { + attributes: { exclude: ['password'] } + } } + }) + + User.prototype.comparePassword = async function (pwd) { + if (!this.password) { return false } + return bcrypt.compare(pwd, this.password) } -}) + + User.beforeSave(async (user, _options) => { + if (user.changed('password')) { + const salt = await bcrypt.genSalt(10) + const hash = await bcrypt.hash(user.password, salt) + user.password = hash + } + }) -User.prototype.comparePassword = async function (pwd) { - if (!this.password) { return false } - return bcrypt.compare(pwd, this.password) + return User } - -User.beforeSave(async (user, _options) => { - if (user.changed('password')) { - const salt = await bcrypt.genSalt(10) - const hash = await bcrypt.hash(user.password, salt) - user.password = hash - } -}) - -module.exports = User diff --git a/server/cli/accounts.js b/server/cli/accounts.js index e41611c6..c1c9bca6 100644 --- a/server/cli/accounts.js +++ b/server/cli/accounts.js @@ -9,7 +9,7 @@ function _initializeDB () { async function modify (args) { await _initializeDB() const helpers = require('../helpers') - const User = require('../api/models/user') + const { User } = require('../api/models/models') const user = await User.findOne({ where: { email: args.account } }) console.log() if (!user) { diff --git a/server/federation/helpers.js b/server/federation/helpers.js index a84b0c70..5454b35d 100644 --- a/server/federation/helpers.js +++ b/server/federation/helpers.js @@ -1,10 +1,10 @@ const axios = require('axios') -// const request = require('request') const crypto = require('crypto') const config = require('../config') const httpSignature = require('http-signature') -const APUser = require('../api/models/ap_user') -const Instance = require('../api/models/instance') + +const { APUser, Instance } = require('../api/models/models') + const url = require('url') const settingsController = require('../api/controller/settings') const log = require('../log') diff --git a/server/helpers.js b/server/helpers.js index 5fa385f5..17f6a289 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -270,9 +270,9 @@ module.exports = { }, async APRedirect(req, res, next) { + const eventController = require('../server/api/controller/event') const acceptJson = req.accepts('html', 'application/activity+json') === 'application/activity+json' if (acceptJson) { - const eventController = require('../server/api/controller/event') const event = await eventController._get(req.params.slug) if (event) { return res.redirect(`/federation/m/${event.id}`) diff --git a/server/initialize.server.js b/server/initialize.server.js index 51b6d9d4..121555de 100644 --- a/server/initialize.server.js +++ b/server/initialize.server.js @@ -1,4 +1,11 @@ const config = require('../server/config') +const db = require('./api/models/index') +const log = require('../server/log') + +db.initialize() + +const settingsController = require('./api/controller/settings') + const initialize = { // close connections/port/unix socket @@ -19,14 +26,14 @@ const initialize = { }, async start () { - const log = require('../server/log') - const settingsController = require('./api/controller/settings') - const db = require('./api/models/index') const dayjs = require('dayjs') const timezone = require('dayjs/plugin/timezone') dayjs.extend(timezone) if (config.status == 'CONFIGURED') { - await db.initialize() + await db.sequelize.authenticate() + log.debug('Running migrations') + await db.runMigrations() + await settingsController.load() config.status = 'READY' } else { if (process.env.GANCIO_DB_DIALECT) { diff --git a/server/notifier.js b/server/notifier.js index be5e9f56..07d08458 100644 --- a/server/notifier.js +++ b/server/notifier.js @@ -4,14 +4,10 @@ const mail = require('./api/mail') const log = require('./log') const fediverseHelpers = require('./federation/helpers') -const Event = require('./api/models/event') -const Notification = require('./api/models/notification') -const EventNotification = require('./api/models/eventnotification') -const User = require('./api/models/user') -const Place = require('./api/models/place') -const Tag = require('./api/models/tag') -const eventController = require('./api/controller/event') +const { Event, Notification, EventNotification, User, Place, Tag } = require('./api/models/models') + + const settingsController = require('./api/controller/settings') const notifier = { @@ -37,7 +33,36 @@ const notifier = { return Promise.all(promises) }, + async getNotifications(event, action) { + log.debug(`getNotifications ${event.title} ${action}`) + function match(event, filters) { + // matches if no filter specified + if (!filters) { return true } + + // check for visibility + if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) { return false } + + if (!filters.tags && !filters.places) { return true } + if (!filters.tags.length && !filters.places.length) { return true } + if (filters.tags.length) { + const m = intersection(event.tags.map(t => t.tag), filters.tags) + if (m.length > 0) { return true } + } + if (filters.places.length) { + if (filters.places.find(p => p === event.place.name)) { + return true + } + } + } + + const notifications = await Notification.findAll({ where: { action }, include: [Event] }) + + // get notification that matches with selected event + return notifications.filter(notification => match(event, notification.filters)) + }, + async notifyEvent (action, eventId) { + const event = await Event.findByPk(eventId, { include: [Tag, Place, Notification, User] }) @@ -46,7 +71,7 @@ const notifier = { log.debug(action, event.title) // insert notifications - const notifications = await eventController.getNotifications(event, action) + const notifications = await notifier.getNotifications(event, action) await event.addNotifications(notifications) const event_notifications = await event.getNotifications({ through: { where: { status: 'new' } } }) diff --git a/server/routes.js b/server/routes.js index c61e6d27..5863cc0a 100644 --- a/server/routes.js +++ b/server/routes.js @@ -4,23 +4,22 @@ const initialize = require('./initialize.server') const config = require('./config') const helpers = require('./helpers') - -app.use([ - helpers.initSettings, - helpers.logRequest, - helpers.serveStatic() -]) +const api = require('./api') async function main () { - + await initialize.start() - + + app.use([ + helpers.initSettings, + helpers.logRequest, + helpers.serveStatic() + ]) // const metricsController = require('./metrics') // const promBundle = require('express-prom-bundle') // const metricsMiddleware = promBundle({ includeMethod: true }) const log = require('./log') - const api = require('./api') app.enable('trust proxy') @@ -60,7 +59,7 @@ async function main () { } // api! - app.use('/api', api) + app.use('/api', api()) // // Handle 500 app.use((error, _req, res, _next) => { @@ -87,8 +86,6 @@ if (process.env.NODE_ENV !== 'test') { main() } -// app.listen(13120) - module.exports = { main, handler: app, diff --git a/yarn.lock b/yarn.lock index 195be9a9..bc6cebae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10634,10 +10634,10 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -retry-as-promised@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-6.1.0.tgz#11eca9a0f97804d552ec8e74bc4eb839bd226dc4" - integrity sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA== +retry-as-promised@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-7.0.3.tgz#ca3c13b15525a7bfbf0f56d2996f0e75649d068b" + integrity sha512-SEvMa4khHvpU/o6zgh7sK24qm6rxVgKnrSyzb5POeDvZx5N9Bf0s5sQsQ4Fl+HjRp0X+w2UzACGfUnXtx6cJ9Q== retry@^0.12.0: version "0.12.0" @@ -10926,10 +10926,10 @@ sequelize-slugify@^1.6.2: dependencies: sluglife "^0.9.8" -sequelize@^6.27.0: - version "6.27.0" - resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.27.0.tgz#b267e76997df57842cc1e2c1c1d7e02405bcdb9c" - integrity sha512-Rm7BM8HQekeABup0KdtSHriu8ppJuHj2TJWCxvZtzU6j8V1LVnBk2rs38P8r4gMWgdLKs5NYoLC4il95KLsv0w== +sequelize@^6.28.0: + version "6.28.0" + resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.28.0.tgz#d6bc4e36647e8501635467c0777c45a33f5d5ba8" + integrity sha512-+WHqvUQgTp19GLkt+gyQ+F6qg+FIEO2O5F9C0TOYV/PjZ2a/XwWvVkL1NCkS4VSIjVVvAUutiW6Wv9ofveGaVw== dependencies: "@types/debug" "^4.1.7" "@types/validator" "^13.7.1" @@ -10940,7 +10940,7 @@ sequelize@^6.27.0: moment "^2.29.1" moment-timezone "^0.5.34" pg-connection-string "^2.5.0" - retry-as-promised "^6.1.0" + retry-as-promised "^7.0.3" semver "^7.3.5" sequelize-pool "^7.1.0" toposort-class "^1.0.1" From 34ca8fc124ed1b4e316496b292dca7019c175d1c Mon Sep 17 00:00:00 2001 From: lesion Date: Fri, 23 Dec 2022 01:09:18 +0100 Subject: [PATCH 062/118] allow html in confirm dialog text --- components/Confirm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Confirm.vue b/components/Confirm.vue index 0c8f2919..05ddc28f 100644 --- a/components/Confirm.vue +++ b/components/Confirm.vue @@ -9,7 +9,7 @@ v-dialog(v-model='show' @keydown.esc='cancel') v-card v-card-title {{ title }} - v-card-text(v-show='!!message') {{ message }} + v-card-text(v-show='!!message' v-html='message') v-card-actions v-spacer v-btn(outlined color='error' @click='cancel') {{$t('common.cancel')}} From fb72f6d693cf29d0c496eee53c980a42014c0f01 Mon Sep 17 00:00:00 2001 From: lesion Date: Fri, 23 Dec 2022 01:11:57 +0100 Subject: [PATCH 063/118] check for duplicates filter on collections + loading fix #218 --- components/admin/Collections.vue | 22 ++++++++++++++++++---- components/admin/Tags.vue | 3 +-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/components/admin/Collections.vue b/components/admin/Collections.vue index 5dd87531..2f9da6a5 100644 --- a/components/admin/Collections.vue +++ b/components/admin/Collections.vue @@ -69,7 +69,7 @@ v-container //- v-list-item-subtitle(v-text='item.address') v-col(cols=2) - v-btn(color='primary' text @click='addFilter' :disabled='!collection.id || !filterPlaces.length && !filterTags.length') add + v-btn(color='primary' :loading='loading' text @click='addFilter' :disabled='loading || !collection.id || !filterPlaces.length && !filterTags.length') add v-data-table( :headers='filterHeaders' @@ -110,6 +110,9 @@ v-container \ No newline at end of file + From 901c11e6ccd6a0eea362729ae50c049555df5315 Mon Sep 17 00:00:00 2001 From: lesion Date: Mon, 9 Jan 2023 16:56:01 +0100 Subject: [PATCH 086/118] calendar loading.. --- components/Calendar.vue | 9 +++++---- components/DateInput.vue | 21 ++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/components/Calendar.vue b/components/Calendar.vue index 1bdca9f8..397fb635 100644 --- a/components/Calendar.vue +++ b/components/Calendar.vue @@ -15,13 +15,14 @@ aria-label='Calendar' is-expanded is-inline) - template(v-slot="{ inputValue, inputEvents }") + //- template(v-slot="{ inputValue, inputEvents }") v-btn#calendarButton(v-on='inputEvents' text tile :color='selectedDate ? "primary" : "" ') {{inputValue || $t('common.calendar')}} v-icon(v-if='selectedDate' v-text='mdiClose' right small icon @click.prevent.stop='selectedDate = null') v-icon(v-else v-text='mdiChevronDown' right small icon) - template(v-slot:placeholder) - v-btn#calendarButton(text tile) {{$t('common.calendar')}} - v-icon(v-text='mdiChevronDown' right small icon) + .calh.d-flex.justify-center.align-center(slot='placeholder') + v-progress-circular(indeterminate) + //- v-btn#calendarButton(text tile) {{$t('common.calendar')}} + //- v-icon(v-text='mdiChevronDown' right small icon) diff --git a/components/DateInput.vue b/components/DateInput.vue index 9166e2c7..2b45b374 100644 --- a/components/DateInput.vue +++ b/components/DateInput.vue @@ -24,8 +24,9 @@ v-col(cols=12) is-inline is-expanded :min-date='type !== "recurrent" && new Date()') - template(#placeholder) - span.calc Loading + //- template(#placeholder) + .d-flex.calh.justify-center(slot='placeholder') + v-progress-circular(indeterminate) div.text-center.mb-2(v-if='type === "recurrent"') span(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') {{ whenPatterns }} @@ -94,7 +95,7 @@ v-col(cols=12) \ No newline at end of file + diff --git a/components/WhereInput.vue b/components/WhereInput.vue index 1b41a0ee..75eb3615 100644 --- a/components/WhereInput.vue +++ b/components/WhereInput.vue @@ -125,9 +125,14 @@ export default { return matches } }, + mounted () { + this.$nextTick( () => { + this.search() + }) + }, methods: { search: debounce(async function(ev) { - const search = ev.target.value.trim().toLowerCase() + const search = ev ? ev.target.value.trim().toLowerCase() : '' this.places = await this.$axios.$get(`place?search=${search}`) if (!search && this.places.length) { return this.places } const matches = this.places.find(p => search === p.name.toLocaleLowerCase()) diff --git a/locales/en.json b/locales/en.json index cc9756c5..ae87414a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -147,6 +147,7 @@ "recurrent": "Recurring", "edit_recurrent": "Edit recurring event:", "show_recurrent": "recurring events", + "show_multidate": "multidate events", "show_past": "also prior events", "only_future": "only upcoming events", "recurrent_description": "Choose frequency and select days", diff --git a/pages/add/_edit.vue b/pages/add/_edit.vue index 5c58d976..8132de1a 100644 --- a/pages/add/_edit.vue +++ b/pages/add/_edit.vue @@ -179,7 +179,6 @@ export default { filteredTags() { if (!this.tagName) { return this.tags.slice(0, 10).map(t => t.tag) } const tagName = this.tagName.trim().toLowerCase() - console.log(tagName) return this.tags.filter(t => t.tag.toLowerCase().includes(tagName)).map(t => t.tag) } }, @@ -245,6 +244,8 @@ export default { if (this.date.dueHour) { [hour, minute] = this.date.dueHour.split(':') formData.append('end_datetime', dayjs(this.date.due).hour(Number(hour)).minute(Number(minute)).second(0).unix()) + } else if (!!this.date.multidate) { + formData.append('end_datetime', dayjs(this.date.due).hour(24).minute(0).second(0).unix()) } if (this.edit) { diff --git a/pages/index.vue b/pages/index.vue index b127c77a..33e1dac6 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,6 +1,5 @@