From 9f748423c95abed4469e242c38746457af5dfdf3 Mon Sep 17 00:00:00 2001 From: lesion Date: Fri, 15 Jul 2022 20:42:27 +0200 Subject: [PATCH 001/171] start with plugins --- server/api/controller/settings.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/api/controller/settings.js b/server/api/controller/settings.js index db13d036..f10248c2 100644 --- a/server/api/controller/settings.js +++ b/server/api/controller/settings.js @@ -40,6 +40,7 @@ const defaultSettings = { { href: '/', label: 'home' }, { href: '/about', label: 'about' } ], + plugins: [], admin_email: config.admin_email || '', smtp: config.smtp || {} } @@ -117,6 +118,7 @@ const settingsController = { const plugin = require(path.resolve(plugins_path, pluginFile)) if (typeof plugin.load !== 'function') return plugin.load({ settings: settingsController.settings }) + settingsController.settings.plugins.push(plugin) log.info(`Plugin ${pluginFile} loaded!`) if (typeof plugin.onEventCreate === 'function') { notifier.emitter.on('Create', plugin.onEventCreate) From 10ba78de9694ec0848afcd1f98684e1974fc85c4 Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 9 Aug 2022 18:32:47 +0200 Subject: [PATCH 002/171] a new plugins controller --- .gitmodules | 3 ++ gancio_plugins/gancio-plugin-telegram-bridge | 1 + server/api/controller/plugins.js | 53 ++++++++++++++++++++ server/api/controller/settings.js | 29 +---------- server/api/index.js | 4 ++ 5 files changed, 63 insertions(+), 27 deletions(-) create mode 100644 .gitmodules create mode 160000 gancio_plugins/gancio-plugin-telegram-bridge create mode 100644 server/api/controller/plugins.js diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c1935240 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "gancio_plugins/gancio-plugin-telegram-bridge"] + path = gancio_plugins/gancio-plugin-telegram-bridge + url = https://framagit.org/les/gancio-plugin-telegram-bridge.git diff --git a/gancio_plugins/gancio-plugin-telegram-bridge b/gancio_plugins/gancio-plugin-telegram-bridge new file mode 160000 index 00000000..a079bff7 --- /dev/null +++ b/gancio_plugins/gancio-plugin-telegram-bridge @@ -0,0 +1 @@ +Subproject commit a079bff738064a08cf00c51b1d09b855265f6306 diff --git a/server/api/controller/plugins.js b/server/api/controller/plugins.js new file mode 100644 index 00000000..60f0d36b --- /dev/null +++ b/server/api/controller/plugins.js @@ -0,0 +1,53 @@ +const path = require('path') +const fs = require('fs') +const log = require('../../log') +const config = require('../../config') + +const pluginController = { + plugins: [], + getAll (req, res, next) { + res.json(pluginController.plugins) + }, + _load () { + const settingsController = require('./settings') + // load custom plugins + const plugins_path = config.plugins_path || path.resolve(process.env.cwd || '', 'gancio_plugins') + log.info(`Loading plugin ${plugins_path}`) + if (fs.existsSync(plugins_path)) { + const notifier = require('../../notifier') + const plugins = fs.readdirSync(plugins_path) + .map(e => path.resolve(plugins_path, e, 'index.js')) + .filter(index => fs.existsSync(index)) + plugins.forEach( pluginFile => { + try { + const plugin = require(pluginFile) + if (typeof plugin.load !== 'function') return + const name = plugin.configuration.name + console.log(`Found plugin '${name}'`) + pluginController.plugins.push(plugin.configuration) + if (settingsController.settings['plugin_' + name]) { + const pluginSetting = settingsController.settings['plugin_' + name] + if (pluginSetting.enable) { + plugin.load({ settings: settingsController.settings }, settingsController.settings.plugins) + if (typeof plugin.onEventCreate === 'function') { + notifier.emitter.on('Create', plugin.onEventCreate) + } + if (typeof plugin.onEventDelete === 'function') { + notifier.emitter.on('Delete', plugin.onEventDelete) + } + if (typeof plugin.onEventUpdate === 'function') { + notifier.emitter.on('Update', plugin.onEventUpdate) + } + } + } else { + settingsController.set('plugin_' + name, { enable: false }) + } + } catch (e) { + log.warn(`Unable to load plugin ${pluginFile}: ${String(e)}`) + } + }) + } + } +} + +module.exports = pluginController \ No newline at end of file diff --git a/server/api/controller/settings.js b/server/api/controller/settings.js index f10248c2..2682eb25 100644 --- a/server/api/controller/settings.js +++ b/server/api/controller/settings.js @@ -9,7 +9,7 @@ const generateKeyPair = promisify(crypto.generateKeyPair) const log = require('../../log') const locales = require('../../../locales/index') const escape = require('lodash/escape') - +const pluginController = require('./plugins') let defaultHostname try { @@ -108,32 +108,7 @@ const settingsController = { }) } - // load custom plugins - const plugins_path = path.resolve(process.env.cwd || '', 'plugins') - if (process.env.NODE_ENV === 'production' && fs.existsSync(plugins_path)) { - const notifier = require('../../notifier') - const pluginsFile = fs.readdirSync(plugins_path).filter(e => path.extname(e).toLowerCase() === '.js') - pluginsFile.forEach( pluginFile => { - try { - const plugin = require(path.resolve(plugins_path, pluginFile)) - if (typeof plugin.load !== 'function') return - plugin.load({ settings: settingsController.settings }) - settingsController.settings.plugins.push(plugin) - log.info(`Plugin ${pluginFile} loaded!`) - if (typeof plugin.onEventCreate === 'function') { - notifier.emitter.on('Create', plugin.onEventCreate) - } - if (typeof plugin.onEventDelete === 'function') { - notifier.emitter.on('Delete', plugin.onEventDelete) - } - if (typeof plugin.onEventUpdate === 'function') { - notifier.emitter.on('Update', plugin.onEventUpdate) - } - } catch (e) { - log.warn(`Unable to load plugin ${pluginFile}: ${String(e)}`) - } - }) - } + pluginController._load() }, async set (key, value, is_secret = false) { diff --git a/server/api/index.js b/server/api/index.js index 8a014cf6..c3ca9cc1 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -34,6 +34,7 @@ if (config.status !== 'READY') { 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 }) @@ -189,6 +190,9 @@ if (config.status !== 'READY') { api.post('/filter', isAdmin, collectionController.addFilter) api.delete('/filter/:id', isAdmin, collectionController.removeFilter) + // - PLUGINS + api.get('/plugins', isAdmin, pluginController.getAll) + // OAUTH api.get('/clients', isAuth, oauthController.getClients) api.get('/client/:client_id', isAuth, oauthController.getClient) From 438780a7415f96aa647e37f1eb992549d1de7bc1 Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 9 Aug 2022 18:33:05 +0200 Subject: [PATCH 003/171] add admin plugin page --- components/admin/Plugin.vue | 70 +++++++++++++++++++++++++++++++++++++ pages/Admin.vue | 6 ++++ 2 files changed, 76 insertions(+) create mode 100644 components/admin/Plugin.vue diff --git a/components/admin/Plugin.vue b/components/admin/Plugin.vue new file mode 100644 index 00000000..79e576a5 --- /dev/null +++ b/components/admin/Plugin.vue @@ -0,0 +1,70 @@ + + diff --git a/pages/Admin.vue b/pages/Admin.vue index 50509723..50d7fe3a 100644 --- a/pages/Admin.vue +++ b/pages/Admin.vue @@ -43,6 +43,11 @@ v-container.container.pa-0.pa-md-3 v-tab-item Announcement + //- PLUGINS + v-tab {{$t('common.plugins')}} + v-tab-item + Plugin + //- FEDERATION v-tab {{$t('common.federation')}} v-tab-item @@ -68,6 +73,7 @@ export default { Collections: () => import(/* webpackChunkName: "admin" */'../components/admin/Collections'), Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'), Moderation: () => import(/* webpackChunkName: "admin" */'../components/admin/Moderation.vue'), + Plugin: () => import(/* webpackChunkName: "admin" */'../components/admin/Plugin.vue'), Announcement: () => import(/* webpackChunkName: "admin" */'../components/admin/Announcement.vue'), Theme: () => import(/* webpackChunkName: "admin" */'../components/admin/Theme.vue') }, From 982db2b51d24e248faf571995719a60b9b310197 Mon Sep 17 00:00:00 2001 From: lesion Date: Mon, 29 Aug 2022 22:47:09 +0200 Subject: [PATCH 004/171] save plugin settings and toggle enable/disable --- components/admin/Plugin.vue | 64 ++++++++-------- gancio_plugins/gancio-plugin-telegram-bridge | 2 +- package.json | 1 + server/api/controller/plugins.js | 42 +++++++++-- yarn.lock | 77 ++++++++++++++++++++ 5 files changed, 150 insertions(+), 36 deletions(-) diff --git a/components/admin/Plugin.vue b/components/admin/Plugin.vue index 79e576a5..8ad4a789 100644 --- a/components/admin/Plugin.vue +++ b/components/admin/Plugin.vue @@ -1,44 +1,48 @@ diff --git a/components/admin/Places.vue b/components/admin/Places.vue index 4e9b0d14..a68ca498 100644 --- a/components/admin/Places.vue +++ b/components/admin/Places.vue @@ -25,6 +25,13 @@ v-container v-model='place.address' :placeholder='$t("common.address")') + v-textarea(v-if="settings.allow_geolocalization" + row-height="15" + :disabled="true" + :label="$t('common.details')" + v-model='place.details' + :placeholder='$t("common.details")') + v-card-actions v-spacer v-btn(@click='dialog=false' color='warning') {{$t('common.cancel')}} @@ -47,6 +54,7 @@ v-container + + diff --git a/components/WhereInput.vue b/components/WhereInput.vue index 088e39fe..75d41da2 100644 --- a/components/WhereInput.vue +++ b/components/WhereInput.vue @@ -47,7 +47,7 @@ v-row v-list-item-content(two-line v-if='item') v-list-item-title(v-text='item.display_name') v-list-item-subtitle(v-text='`${item.lat}`+`,`+`${item.lon}`') - v-text-field(ref='details' v-show='false' v-if='settings.allow_geolocation') + v-text-field(ref='details' v-if='settings.allow_geolocation') From 6a7205ed3462a26c2025d79d4ead2ac86dce57db Mon Sep 17 00:00:00 2001 From: sedum Date: Fri, 28 Oct 2022 04:36:39 +0200 Subject: [PATCH 029/171] allowgeoloc: trim placename before submit as issue 189 reported --- pages/add/_edit.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/add/_edit.vue b/pages/add/_edit.vue index c18a7ad1..1e9377d3 100644 --- a/pages/add/_edit.vue +++ b/pages/add/_edit.vue @@ -212,7 +212,7 @@ export default { if (this.event.place.id) { formData.append('place_id', this.event.place.id) } - formData.append('place_name', this.event.place.name) + formData.append('place_name', this.event.place.name.trim()) formData.append('place_address', this.event.place.address) formData.append('place_latitude', this.event.place.latitude) formData.append('place_longitude', this.event.place.longitude) From ca6a0ea58f699e97898fe1fa8c8daa0692e78e4b Mon Sep 17 00:00:00 2001 From: sedum Date: Fri, 28 Oct 2022 08:27:47 +0200 Subject: [PATCH 030/171] center map in dialog delaying popup, add validation of coordinates in /add, fix loading feedback in searchCoordinates field --- components/Map.vue | 157 ++++++++++++++++++++------------------ components/WhereInput.vue | 30 ++++---- pages/event/_slug.vue | 2 +- 3 files changed, 96 insertions(+), 93 deletions(-) diff --git a/components/Map.vue b/components/Map.vue index 19ed9fe0..9846c9cc 100644 --- a/components/Map.vue +++ b/components/Map.vue @@ -19,8 +19,8 @@ client-only(placeholder='Loading...' ) .text-h6 v-icon(v-text='mdiMapMarker' ) nuxt-link.ml-2.p-name.text-decoration-none(v-text="event.place.name" :to='`/place/${event.place.name}`') - v-text.mx-2(v-text="`${event.place.address}`") - v-text.my-4(v-text="$t('common.getting_there')") + p.mx-2(v-text="`${event.place.address}`") + p.my-4(v-text="$t('common.getting_there')") v-row v-btn.ml-2(icon large :href="routeBy('foot')") v-icon(v-text='mdiWalk' color='white') @@ -32,87 +32,94 @@ client-only(placeholder='Loading...' ) diff --git a/components/WhereInput.vue b/components/WhereInput.vue index fcde2108..fa9ec92d 100644 --- a/components/WhereInput.vue +++ b/components/WhereInput.vue @@ -173,27 +173,24 @@ export default { this.$emit('input', { ...this.place }) }, searchCoordinates: debounce(async function(ev) { - this.loading = true const pre_searchCoordinates = ev.target.value.trim().toLowerCase() - // allow pasting coordinates lat/lon + // allow pasting coordinates lat/lon and lat,lon const searchCoordinates = pre_searchCoordinates.replace('/', ',') - // console.log(pre_searchCoordinates) - var regex_coords_comma = "-?[1-9][0-9]*(\\.[0-9]+)?,\\s*-?[1-9][0-9]*(\\.[0-9]+)?"; var regex_coords_slash = "-?[1-9][0-9]*(\\.[0-9]+)?/\\s*-?[1-9][0-9]*(\\.[0-9]+)?"; const setCoords = (v) => { - this.place.latitude = v[0].trim() - this.place.longitude = v[1].trim() - - if (this.place.latitude < -90 || this.place.latitude > 90) { - // non existent + const lat = v[0].trim() + const lon = v[1].trim() + // check coordinates are valid + if ((lat < 90 && lat > -90) + && (lon < 180 && lon > -180)) { + this.place.latitude = lat + this.place.longitude = lon + } else { + this.$root.$message("Non existent coordinates", { color: 'error' }) + return } - if (this.place.latitude < -180 || this.place.latitude > 180) { - // non existent - } - - this.loading = false; } if (pre_searchCoordinates.match(regex_coords_comma)) { @@ -208,10 +205,9 @@ export default { } if (searchCoordinates.length) { + this.loading = true this.detailsList = await this.$axios.$get(`placeNominatim/${searchCoordinates}`) - } - if (this.detailsList) { - this.loading = false; + this.loading = false } }, 300), } diff --git a/pages/event/_slug.vue b/pages/event/_slug.vue index b60bd31d..3cc1f86a 100644 --- a/pages/event/_slug.vue +++ b/pages/event/_slug.vue @@ -32,7 +32,7 @@ v-container#event.pa-0.pa-sm-2 v-icon(v-text='mdiMapMarker') nuxt-link.vcard.ml-2.p-name.text-decoration-none(itemprop="name" :to='`/place/${event.place.name}`') {{event.place && event.place.name}} .text-subtitle-1.p-street-address(itemprop='address') {{event.place && event.place.address}} - v-btn.mt-2(v-if='event.place.latitude && event.place.longitude' small v-text="$t('common.show_map')" :aria-label="$t('common.show_map')" @click="mapModal = true") + v-btn.mt-2(v-if='settings.allow_geolocation && event.place.latitude && event.place.longitude' small v-text="$t('common.show_map')" :aria-label="$t('common.show_map')" @click="mapModal = true") v-dialog(v-model='mapModal' :fullscreen='$vuetify.breakpoint.xsOnly' destroy-on-close) v-card Map(:event='event') From 7d0a920eb939c9488fc7a398d62f6eb299896e2e Mon Sep 17 00:00:00 2001 From: lesion Date: Fri, 28 Oct 2022 12:01:59 +0200 Subject: [PATCH 031/171] add custom fallback image, fix #195 --- components/MyPicture.vue | 2 +- components/admin/Theme.vue | 53 ++++++++++++++++++++++----- docs/assets/js/gancio-events.es.js | 4 +- locales/en.json | 3 +- server/api/controller/settings.js | 23 ++++++++++++ server/api/index.js | 1 + server/helpers.js | 9 +++-- static/gancio-events.es.js | 4 +- webcomponents/src/GancioEvents.svelte | 2 +- wp-plugin/js/gancio-events.es.js | 4 +- 10 files changed, 84 insertions(+), 21 deletions(-) diff --git a/components/MyPicture.vue b/components/MyPicture.vue index ef4a863d..6f8c2142 100644 --- a/components/MyPicture.vue +++ b/components/MyPicture.vue @@ -10,7 +10,7 @@ :height="height" :width="width" :style="{ 'object-position': thumbnailPosition }"> - + - + \ No newline at end of file diff --git a/pages/add/_edit.vue b/pages/add/_edit.vue index 092b4d54..bbc281a1 100644 --- a/pages/add/_edit.vue +++ b/pages/add/_edit.vue @@ -217,7 +217,9 @@ export default { formData.append('description', this.event.description) formData.append('multidate', !!this.date.multidate) formData.append('start_datetime', dayjs(this.date.from).unix()) - formData.append('end_datetime', this.date.due ? dayjs(this.date.due).unix() : dayjs(this.date.from).add(2, 'hour').unix()) + if (this.date.due) { + formData.append('end_datetime', dayjs(this.date.due).unix()) + } if (this.edit) { formData.append('id', this.event.id) diff --git a/pages/index.vue b/pages/index.vue index 1d053502..f94fcfb7 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -56,7 +56,6 @@ export default { data ({ $store }) { return { mdiMagnify, mdiCloseCircle, - first: true, isCurrentMonth: true, now: dayjs().unix(), date: dayjs.tz().format('YYYY-MM-DD'), @@ -93,7 +92,7 @@ export default { const max = dayjs.tz(this.selectedDay).endOf('day').unix() return this.events.filter(e => (e.start_datetime <= max && (e.end_datetime || e.start_datetime) >= min) && (this.show_recurrent || !e.parentId)) } else if (this.isCurrentMonth) { - return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 2 * 60 * 60 > now) && (this.show_recurrent || !e.parentId))) + return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 3 * 60 * 60 > now) && (this.show_recurrent || !e.parentId))) } else { return this.events.filter(e => this.show_recurrent || !e.parentId) } @@ -115,11 +114,6 @@ export default { }) }, monthChange ({ year, month }) { - // avoid first time monthChange event (onload) - if (this.first) { - this.first = false - return - } this.$nuxt.$loading.start() diff --git a/plugins/filters.js b/plugins/filters.js index ac6e3051..675b8eaf 100644 --- a/plugins/filters.js +++ b/plugins/filters.js @@ -77,13 +77,12 @@ export default ({ app, store }) => { Vue.filter('when', (event) => { const start = dayjs.unix(event.start_datetime).tz().locale(app.i18n.locale || store.state.settings.instance_locale) - const end = dayjs.unix(event.end_datetime).tz().locale(app.i18n.locale || store.state.settings.instance_locale) - // multidate - if (event.multidate) { - return `${start.format('dddd D MMMM HH:mm')} - ${end.format('dddd D MMMM HH:mm')}` - } + const end = event.end_datetime && dayjs.unix(event.end_datetime).tz().locale(app.i18n.locale || store.state.settings.instance_locale) - // normal event - return `${start.format('dddd D MMMM HH:mm')}-${end.format('HH:mm')}` + let time = start.format('dddd D MMMM HH:mm') + if (end) { + time += event.multidate ? `-${end.format('dddd D MMMM HH:mm')}` : `-${end.format('HH:mm')}` + } + return time }) } diff --git a/server/api/auth.js b/server/api/auth.js index e24a30a2..65f199a6 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -1,28 +1,138 @@ const log = require('../log') -const oauth = require('./oauth') const get = require('lodash/get') +const passport = require('passport') + +// const oauth = require('./oauth') +// const User = require('./models/user') +// const OAuthClient = require('./models/oauth_client') +// const OAuthCode = require('./models/oauth_code') +// const OAuthToken = require('./models/oauth_token') + + +// const CustomStrategy = require('passport-custom').Strategy +// const LocalStrategy = require('passport-local').Strategy +// const BasicStrategy = require('passport-http').BasicStrategy +// const ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy +// const BearerStrategy = require('passport-http-bearer').Strategy + +// console.error('dentro passport setup!') +// passport.use('authenticate', new CustomStrategy(async (req, done) => { +// console.error('dentro authenticate strategy') + +// // check if a cookie is passed +// const token = get(req.cookies, 'auth._token.local', null) +// const authorization = get(req.headers, 'authorization', null) +// if (!authorization && token) { +// req.headers.authorization = token +// } + +// if (!authorization && !token) { +// return done(null, false) +// } + +// console.error(authorization, token) +// return done(null, false) + +// })) + +/** + * LocalStrategy + * + * This strategy is used to authenticate users based on a username and password. + * Anytime a request is made to authorize an application, we must ensure that + * a user is logged in before asking them to approve the request. + */ +// passport.use(new LocalStrategy( +// async (username, password, done) => { +// console.error(`sono qui dentro local strategy cerco ${username} ${password}}`) +// const user = await User.findOne({ where: { email: username, is_active: true } }) +// console.error(user) +// if (!user) { +// return done(null, false) +// } +// // check if password matches +// if (await user.comparePassword(password)) { +// console.error('compare password ok!') +// return done(null, user) +// } +// return done(null, false) +// } +// // )) + +// passport.serializeUser((user, done) => done(null, user.id)) + +// passport.deserializeUser(async (id, done) => { +// const user = await User.findByPk(id) +// done(null, user) +// }) + +/** + * BasicStrategy & ClientPasswordStrategy + * + * These strategies are used to authenticate registered OAuth clients. They are + * employed to protect the `token` endpoint, which consumers use to obtain + * access tokens. The OAuth 2.0 specification suggests that clients use the + * HTTP Basic scheme to authenticate. Use of the client password strategy + * allows clients to send the same credentials in the request body (as opposed + * to the `Authorization` header). While this approach is not recommended by + * the specification, in practice it is quite common. + */ +// async function verifyClient(client_id, client_secret, done) { +// console.error('Dentro verify client ', client_id, client_secret) +// const client = await OAuthClient.findByPk(client_id, { raw: true }) +// console.error(client) +// if (client_secret && client_secret !== client.client_secret) { +// return done(null, false) +// } + +// if (client) { client.grants = ['authorization_code', 'password'] } //sure ? + +// return done(null, client) +// } + +// passport.use(new BasicStrategy(verifyClient)) +// passport.use(new ClientPasswordStrategy(verifyClient)) + +/** + * BearerStrategy + * + * This strategy is used to authenticate either users or clients based on an access token + * (aka a bearer token). If a user, they must have previously authorized a client + * application, which is issued an access token to make requests on behalf of + * the authorizing user. + */ +// passport.use(new BearerStrategy( +// async (accessToken, done) => { +// console.error('dentro bearer strategy') +// const token = await OAuthToken.findByPk(accessToken, +// { include: [{ model: User, attributes: { exclude: ['password'] } }, { model: OAuthClient, as: 'client' }] }) + +// if (!token) return done(null, false) +// if (token.userId) { +// if (!token.user) { +// return done(null, false) +// } +// // To keep this example simple, restricted scopes are not implemented, +// // and this is just for illustrative purposes. +// done(null, user, { scope: '*' }) +// } else { +// // The request came from a client only since userId is null, +// // therefore the client is passed back instead of a user. +// if (!token.client) { +// return done(null, false) +// } +// // To keep this example simple, restricted scopes are not implemented, +// // and this is just for illustrative purposes. +// done(null, client, { scope: '*' }) +// } +// } +// )) const Auth = { - fillUser (req, res, next) { - const token = get(req.cookies, 'auth._token.local', null) - const authorization = get(req.headers, 'authorization', null) - if (!authorization && token) { - req.headers.authorization = token - } - - if (!authorization && !token) { - return next() - } - - oauth.oauthServer.authenticate()(req, res, () => { - res.locals.user = get(res, 'locals.oauth.token.user', null) - next() - }) - }, - - isAuth (_req, res, next) { - if (res.locals.user) { + isAuth (req, res, next) { + // TODO: check anon user + if (req.user) { next() } else { res.sendStatus(403) @@ -30,7 +140,7 @@ const Auth = { }, isAdmin (req, res, next) { - if (res.locals.user && res.locals.user.is_admin) { + if (req.user && req.user.is_admin) { next() } else { res.sendStatus(403) diff --git a/server/api/controller/event.js b/server/api/controller/event.js index 063dc6cb..91b40d84 100644 --- a/server/api/controller/event.js +++ b/server/api/controller/event.js @@ -196,7 +196,7 @@ const eventController = { async get(req, res) { const format = req.params.format || 'json' - const is_admin = res.locals.user && res.locals.user.is_admin + const is_admin = req.user && req.user.is_admin const slug = req.params.event_slug // retrocompatibility, old events URL does not use slug, use id as fallback @@ -301,7 +301,7 @@ const eventController = { log.warn(`Trying to confirm a unknown event, id: ${id}`) return res.sendStatus(404) } - if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) { + if (!req.user.is_admin && req.user.id !== event.userId) { log.warn(`Someone not allowed is trying to confirm -> "${event.title} `) return res.sendStatus(403) } @@ -327,7 +327,7 @@ const eventController = { const id = Number(req.params.event_id) const event = await Event.findByPk(id) if (!event) { return req.sendStatus(404) } - if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) { + if (!req.user.is_admin && req.user.id !== event.userId) { log.warn(`Someone not allowed is trying to unconfirm -> "${event.title} `) return res.sendStatus(403) } @@ -386,8 +386,8 @@ const eventController = { res.sendStatus(200) }, - async isAnonEventAllowed(_req, res, next) { - if (!res.locals.settings.allow_anon_event && !res.locals.user) { + async isAnonEventAllowed(req, res, next) { + if (!res.locals.settings.allow_anon_event && !req.user) { return res.sendStatus(403) } next() @@ -432,7 +432,7 @@ const eventController = { end_datetime: body.end_datetime, recurrent, // publish this event only if authenticated - is_visible: !!res.locals.user + is_visible: !!req.user } if (req.file || body.image_url) { @@ -466,9 +466,9 @@ const eventController = { } // associate user to event and reverse - if (res.locals.user) { - await res.locals.user.addEvent(event) - await event.setUser(res.locals.user) + if (req.user) { + await req.user.addEvent(event) + await event.setUser(req.user) } event = event.get() @@ -502,7 +502,7 @@ const eventController = { const body = req.body const event = await Event.findByPk(body.id) if (!event) { return res.sendStatus(404) } - if (!res.locals.user.is_admin && event.userId !== res.locals.user.id) { + if (!req.user.is_admin && event.userId !== req.user.id) { return res.sendStatus(403) } @@ -596,7 +596,7 @@ const eventController = { async remove(req, res) { const event = await Event.findByPk(req.params.id) // check if event is mine (or user is admin) - if (event && (res.locals.user.is_admin || res.locals.user.id === event.userId)) { + if (event && (req.user.is_admin || req.user.id === event.userId)) { if (event.media && event.media.length && !event.recurrent) { try { const old_path = path.join(config.upload_path, event.media[0].url) diff --git a/server/api/controller/oauth.js b/server/api/controller/oauth.js index 77eabafe..d40a6cb0 100644 --- a/server/api/controller/oauth.js +++ b/server/api/controller/oauth.js @@ -1,26 +1,357 @@ -const crypto = require('crypto') -const { promisify } = require('util') -const randomBytes = promisify(crypto.randomBytes) +const bodyParser = require('body-parser') +const cookieParser = require('cookie-parser') +const session = require('express-session') const OAuthClient = require('../models/oauth_client') const OAuthToken = require('../models/oauth_token') const OAuthCode = require('../models/oauth_code') + +const helpers = require('../../helpers.js') const User = require('../models/user') +const passport = require('passport') +const get = require('lodash/get') + +const BasicStrategy = require('passport-http').BasicStrategy +const ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy +const ClientPublicStrategy = require('passport-oauth2-client-public').Strategy +const BearerStrategy = require('passport-http-bearer').Strategy +const AnonymousStrategy = require('passport-anonymous').Strategy + +const oauth2orize = require('oauth2orize') const log = require('../../log') -const dayjs = require('dayjs') -async function randomString (len = 16) { - const bytes = await randomBytes(len * 8) - return crypto - .createHash('sha1') - .update(bytes) - .digest('hex') +passport.serializeUser((user, done) => done(null, user.id)) + +passport.deserializeUser(async (id, done) => { + const user = await User.findByPk(id) + done(null, user) +}) + +/** + * BasicStrategy & ClientPasswordStrategy + * + * These strategies are used to authenticate registered OAuth clients. They are + * employed to protect the `token` endpoint, which consumers use to obtain + * access tokens. The OAuth 2.0 specification suggests that clients use the + * HTTP Basic scheme to authenticate. Use of the client password strategy + * allows clients to send the same credentials in the request body (as opposed + * to the `Authorization` header). While this approach is not recommended by + * the specification, in practice it is quite common. + */ +async function verifyClient(client_id, client_secret, done) { + const client = await OAuthClient.findByPk(client_id, { raw: true }) + if (!client) { + return done(null, false) + } + if (client.client_secret && client_secret !== client.client_secret) { + return done(null, false) + } + + if (client) { client.grants = ['authorization_code', 'password'] } //sure ? + + return done(null, client) } -const oauthController = { +async function verifyPublicClient (client_id, done) { + if (client_id !== 'self') { + return done(null, false) + } + try { - // create client => http:///gancio.org/oauth#create-client + const client = await OAuthClient.findByPk(client_id, { raw: true }) + done(null, client) + } catch (e) { + done(null, { message: e.message }) + } +} + +passport.use(new AnonymousStrategy()) +passport.use(new BasicStrategy(verifyClient)) +passport.use(new ClientPasswordStrategy(verifyClient)) +passport.use(new ClientPublicStrategy(verifyPublicClient)) + +/** + * BearerStrategy + * + * This strategy is used to authenticate either users or clients based on an access token + * (aka a bearer token). If a user, they must have previously authorized a client + * application, which is issued an access token to make requests on behalf of + * the authorizing user. + */ + passport.use(new BearerStrategy({ passReqToCallback: true }, verifyToken)) + +async function verifyToken (req, accessToken, done) { + const token = await OAuthToken.findByPk(accessToken, + { include: [{ model: User, attributes: { exclude: ['password'] } }, { model: OAuthClient, as: 'client' }] }) + + if (!token) return done(null, false) + if (token.userId) { + if (!token.user) { + return done(null, false) + } + // To keep this example simple, restricted scopes are not implemented, + // and this is just for illustrative purposes. + done(null, token.user, { scope: '*' }) + } else { + + // The request came from a client only since userId is null, + // therefore the client is passed back instead of a user. + if (!token.client) { + return done(null, false) + } + // To keep this example simple, restricted scopes are not implemented, + // and this is just for illustrative purposes. + done(null, client, { scope: '*' }) + } +} + + +const oauthServer = oauth2orize.createServer() + + +// Register serialization and deserialization functions. +// +// When a client redirects a user to user authorization endpoint, an +// authorization transaction is initiated. To complete the transaction, the +// user must authenticate and approve the authorization request. Because this +// may involve multiple HTTP request/response exchanges, the transaction is +// stored in the session. +// +// An application must supply serialization functions, which determine how the +// client object is serialized into the session. Typically this will be a +// simple matter of serializing the client's ID, and deserializing by finding +// the client by ID from the database. +oauthServer.serializeClient((client, done) => { + done(null, client.id) +}) + +oauthServer.deserializeClient(async (id, done) => { + const client = await OAuthClient.findByPk(id) + done(null, client) +}) + +// Register supported grant types. +// +// OAuth 2.0 specifies a framework that allows users to grant client +// applications limited access to their protected resources. It does this +// through a process of the user granting access, and the client exchanging +// the grant for an access token. + +// Grant authorization codes. The callback takes the `client` requesting +// authorization, the `redirectUri` (which is used as a verifier in the +// subsequent exchange), the authenticated `user` granting access, and +// their response, which contains approved scope, duration, etc. as parsed by +// the application. The application issues a code, which is bound to these +// values, and will be exchanged for an access token. + +oauthServer.grant(oauth2orize.grant.code(async (client, redirect_uri, user, ares, done) => { + const authorizationCode = helpers.randomString(16); + await OAuthCode.create({ + redirect_uri, + authorizationCode, + clientId: client.id, + userId: user.id, + }) + return done(null, authorizationCode) +})) + + +// Grant implicit authorization. The callback takes the `client` requesting +// authorization, the authenticated `user` granting access, and +// their response, which contains approved scope, duration, etc. as parsed by +// the application. The application issues a token, which is bound to these +// values. + +oauthServer.grant(oauth2orize.grant.token((client, user, ares, done) => { + return oauthController.issueTokens(user.id, client.clientId, done) +})) + + +// Exchange authorization codes for access tokens. The callback accepts the +// `client`, which is exchanging `code` and any `redirectUri` from the +// authorization request for verification. If these values are validated, the +// application issues an access token on behalf of the user who authorized the +// code. The issued access token response can include a refresh token and +// custom parameters by adding these to the `done()` call + +oauthServer.exchange(oauth2orize.exchange.code(async (client, code, redirect_uri, done) => { + const oauthCode = await OAuthCode.findByPk(code) + if (!oauthCode || client.id !== oauthCode.clientId || client.redirectUris !== oauthCode.redirect_uri) { + return done(null, false) + } + return oauthController.issueTokens(oauthCode.userId, oauthCode.clientId, done) +})) + + + +// Exchange user id and password for access tokens. The callback accepts the +// `client`, which is exchanging the user's name and password from the +// authorization request for verification. If these values are validated, the +// application issues an access token on behalf of the user who authorized the code. +oauthServer.exchange(oauth2orize.exchange.password(async (client, username, password, scope, done) => { + // Validate the client + const oauthClient = await OAuthClient.findByPk(client.id) + if (!oauthClient) { // || oauthClient.client_secret !== client.clientSecret) { + return done(null, false) + } + const user = await User.findOne({ where: { email: username, is_active: true } }) + if (!user) { + return done(null, false) + } + // check if password matches + if (await user.comparePassword(password)) { + return oauthController.issueTokens(user.id, oauthClient.id, done) + } + return done(null, false) +})) + + +// Exchange the client id and password/secret for an access token. The callback accepts the +// `client`, which is exchanging the client's id and password/secret from the +// authorization request for verification. If these values are validated, the +// application issues an access token on behalf of the client who authorized the code. +oauthServer.exchange(oauth2orize.exchange.clientCredentials(async (client, scope, done) => { + // Validate the client + const oauthClient = await OAuthClient.findByPk(client.clientId) + if (!oauthClient || oauthClient.client_secret !== client.clientSecret) { + return done(null, false) + } + + return oauthController.issueTokens(null, oauthClient.id, done) +})) + +// issue new tokens and remove the old ones +oauthServer.exchange(oauth2orize.exchange.refreshToken(async (client, refreshToken, scope, done) => { + // db.refreshTokens.find(refreshToken, (error, token) => { + // if (error) return done(error) + // issueTokens(token.id, client.id, (err, accessToken, refreshToken) => { + // if (err) { + // done(err, null, null) + // } + // db.accessTokens.removeByUserIdAndClientId(token.userId, token.clientId, (err) => { + // if (err) { + // done(err, null, null) + // } + // db.refreshTokens.removeByUserIdAndClientId(token.userId, token.clientId, (err) => { + // if (err) { + // done(err, null, null) + // } + // done(null, accessToken, refreshToken) + // }) + // }) + // }) + // }) +})) + + +const oauthController = { + + // this is a middleware to authenticate a request + authenticate: [ + passport.initialize(), // initialize passport + cookieParser(), // parse cookies + session({ secret: 'secret', resave: true, saveUninitialized: true }), + passport.session(), + (req, res, next) => { // retrocompatibility + const token = get(req.cookies, 'auth._token.local', null) + const authorization = get(req.headers, 'authorization', null) + if (!authorization && token) { + req.headers.authorization = token + } + next() + }, + passport.authenticate(['bearer', 'oauth2-client-password', 'anonymous'], { session: false }) + ], + + login: [ + bodyParser.urlencoded({ extended: true }), // login is done via application/x-www-form-urlencoded form + passport.authenticate(['oauth2-client-public'], { session: false }), + oauthServer.token(), + oauthServer.errorHandler() + ], + + token: [ + bodyParser.urlencoded({ extended: true }), // login is done via application/x-www-form-urlencoded form + passport.authenticate(['bearer', 'oauth2-client-password'], { session: false }), + oauthServer.token(), + oauthServer.errorHandler() + ], + + authorization: [ + oauthServer.authorization(async (clientId, redirectUri, done) => { + const oauthClient = await OAuthClient.findByPk(clientId) + if (!oauthClient) { + return done(null, false) + } + + // WARNING: For security purposes, it is highly advisable to check that + // redirectUri provided by the client matches one registered with + // the server. For simplicity, this example does not. You have + // been warned. + return done(null, oauthClient, redirectUri); + }, async (client, user, done) => { + // Check if grant request qualifies for immediate approval + + // Auto-approve + if (client.isTrusted) return done(null, true); + if (!user) { + return done(null, false) + } + const token = await OAuthToken.findOne({ where: { clientId: client.id, userId: user.id }}) + // Auto-approve + if (token) { + return done(null, true) + } + // Otherwise ask user + return done(null, false) + + }), + (req, res, next) => { + //clean old transactionID + if(req.session.authorize){ + for(const key in req.session.authorize){ + if(key !== req.oauth2.transactionID){ + delete req.session.authorize[key]; + } + } + } + const query = new URLSearchParams({ + transactionID: req.oauth2.transactionID, + client: req.oauth2.client.name, + scope: req.oauth2.client.scopes, + redirect_uri: req.oauth2.client.redirectUris + }) + return res.redirect(`/authorize?${query.toString()}`) + } + ], + + decision: [ + bodyParser.urlencoded({ extended: true }), + oauthServer.decision() + ], + + async issueTokens(userId, clientId, done) { + const user = await User.findByPk(userId) + if (!user) { + return done(null, false) + } + + const refreshToken = helpers.randomString(32) + const accessToken = helpers.randomString(32) + + const token = { + refreshToken, + accessToken, + userId, + clientId + } + + await OAuthToken.create(token) + return done(null, accessToken, refreshToken, { username: user.email }) + }, + + // create client => http:///gancio.org/dev/oauth#create-client async createClient (req, res) { // only write scope is supported if (req.body.scopes && req.body.scopes !== 'event:write') { @@ -28,12 +359,12 @@ const oauthController = { } const client = { - id: await randomString(256), + id: helpers.randomString(32), name: req.body.client_name, redirectUris: req.body.redirect_uris, scopes: req.body.scopes || 'event:write', website: req.body.website, - client_secret: await randomString(256) + client_secret: helpers.randomString(32) } try { @@ -63,99 +394,11 @@ const oauthController = { async getClients (req, res) { const tokens = await OAuthToken.findAll({ - include: [{ model: User, where: { id: res.locals.user.id } }, { model: OAuthClient, as: 'client' }], + include: [{ model: User, where: { id: req.user.id } }, { model: OAuthClient, as: 'client' }], raw: true, nest: true }) res.json(tokens) - }, - - model: { - - /** - * Invoked to retrieve an existing access token previously saved through #saveToken(). - * https://oauth2-server.readthedocs.io/en/latest/model/spec.html#getaccesstoken-accesstoken-callback - * */ - async getAccessToken (accessToken) { - const oauth_token = await OAuthToken.findByPk(accessToken, - { include: [{ model: User, attributes: { exclude: ['password'] } }, { model: OAuthClient, as: 'client' }] }) - return oauth_token - }, - - /** - * Invoked to retrieve a client using a client id or a client id/client secret combination, depend on the grant type. - */ - async getClient (client_id, client_secret) { - const client = await OAuthClient.findByPk(client_id, { raw: true }) - if (!client || (client_secret && client_secret !== client.client_secret)) { - return false - } - - if (client) { client.grants = ['authorization_code', 'password'] } - - return client - }, - - async getRefreshToken (refresh_token) { - const oauth_token = await OAuthToken.findOne({ where: { refresh_token }, raw: true }) - return oauth_token - }, - - async getAuthorizationCode (code) { - const oauth_code = await OAuthCode.findByPk(code, - { include: [User, { model: OAuthClient, as: 'client' }] }) - return oauth_code - }, - - async saveToken (token, client, user) { - token.userId = user.id - token.clientId = client.id - const oauth_token = await OAuthToken.create(token) - oauth_token.client = client - oauth_token.user = user - return oauth_token - }, - - async revokeAuthorizationCode (code) { - const oauth_code = await OAuthCode.findByPk(code.authorizationCode) - return oauth_code.destroy() - }, - - async getUser (username, password) { - const user = await User.findOne({ where: { email: username } }) - if (!user || !user.is_active) { - return false - } - // check if password matches - if (await user.comparePassword(password)) { - return user - } - return false - }, - - async saveAuthorizationCode (code, client, user) { - code.userId = user.id - code.clientId = client.id - code.expiresAt = dayjs(code.expiresAt).toDate() - return OAuthCode.create(code) - }, - - // TODO - verifyScope (token, scope) { - // const userScope = [ - // 'user:remove', - // 'user:update', - // 'event:write', - // 'event:remove' - // ] - log.debug(`VERIFY SCOPE ${scope} ${token.user.email}`) - if (token.user.is_admin && token.user.is_active) { - return true - } else { - return false - } - } - } } diff --git a/server/api/controller/user.js b/server/api/controller/user.js index 7affed42..c1e482ee 100644 --- a/server/api/controller/user.js +++ b/server/api/controller/user.js @@ -44,13 +44,13 @@ const userController = { }, async current (req, res) { - if (!res.locals.user) { return res.status(400).send('Not logged') } - const user = await User.scope('withoutPassword').findByPk(res.locals.user.id) + if (!req.user) { return res.status(400).send('Not logged') } + const user = await User.scope('withoutPassword').findByPk(req.user.id) res.json(user) }, async getAll (req, res) { - const users = await User.scope(res.locals.user.is_admin ? 'withRecover' : 'withoutPassword').findAll({ + const users = await User.scope(req.user.is_admin ? 'withRecover' : 'withoutPassword').findAll({ order: [['is_admin', 'DESC'], ['createdAt', 'DESC']] }) res.json(users) @@ -62,7 +62,7 @@ const userController = { if (!user) { return res.status(404).json({ success: false, message: 'User not found!' }) } - if (req.body.id !== res.locals.user.id && !res.locals.user.is_admin) { + if (req.body.id !== req.user.id && !req.user.is_admin) { return res.status(400).json({ succes: false, message: 'Not allowed' }) } @@ -123,10 +123,10 @@ const userController = { async remove (req, res) { try { let user - if (res.locals.user.is_admin && req.params.id) { + if (req.user.is_admin && req.params.id) { user = await User.findByPk(req.params.id) } else { - user = await User.findByPk(res.locals.user.id) + user = await User.findByPk(req.user.id) } await user.destroy() log.warn(`User ${user.email} removed!`) diff --git a/server/api/index.js b/server/api/index.js index a0946772..12729205 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -60,7 +60,7 @@ if (config.status !== 'READY') { ``` */ api.get('/ping', (_req, res) => res.sendStatus(200)) - api.get('/user', isAuth, (_req, res) => res.json(res.locals.user)) + api.get('/user', isAuth, (req, res) => res.json(req.user)) api.post('/user/recover', userController.forgotPassword) diff --git a/server/api/oauth.js b/server/api/oauth.js index be4ade64..e9091815 100644 --- a/server/api/oauth.js +++ b/server/api/oauth.js @@ -1,41 +1,51 @@ -const express = require('express') -const OAuthServer = require('express-oauth-server') -const oauth = express.Router() -const oauthController = require('./controller/oauth') -const log = require('../log') +// const express = require('express') +// // const OAuthServer = require('express-oauth-server') +// const oauth2orize = require('oauth2orize') +// // const oauth = express.Router() +// // const oauthController = require('./controller/oauth') +// // const OauthClient = require('./models/oauth_client') +// // const log = require('../log') -const oauthServer = new OAuthServer({ - model: oauthController.model, - allowEmptyState: true, - useErrorHandler: true, - continueMiddleware: false, - debug: true, - requireClientAuthentication: { password: false }, - authenticateHandler: { - handle (_req, res) { - if (!res.locals.user) { - throw new Error('Not authenticated!') - } - return res.locals.user - } - } -}) +// // const oauthServer = oauth2orize.createServer() -oauth.oauthServer = oauthServer -oauth.use(express.json()) -oauth.use(express.urlencoded({ extended: false })) +// /* model: oauthController.model, */ +// /* allowEmptyState: true, */ +// /* useErrorHandler: true, */ +// /* continueMiddleware: false, */ +// /* debug: true, */ +// /* requireClientAuthentication: { password: false }, */ +// /* authenticateHandler: { */ +// /* handle (_req, res) { */ +// /* if (!res.locals.user) { */ +// /* throw new Error('Not authenticated!') */ +// /* } */ +// /* return res.locals.user */ +// /* } */ +// /* } */ +// /* }) */ -oauth.post('/token', oauthServer.token()) -oauth.post('/login', oauthServer.token()) +// // oauth.oauthServer = oauthServer +// // oauth.use(express.json()) +// // oauth.use(express.urlencoded({ extended: false })) -oauth.get('/authorize', oauthServer.authorize()) -oauth.use((req, res) => res.sendStatus(404)) +// oauthServer.serializeClient((client, done) => done(null, client.id)) +// oauthServer.deserializeClient(async (id, done) => { +// const client = await OAuthServer.findByPk(id) +// done(null, client) +// }) -oauth.use((err, req, res, next) => { - const error_msg = err.toString() - log.warn('[OAUTH USE] ' + error_msg) - res.status(500).send(error_msg) -}) -module.exports = oauth +// oauth.post('/token', oauthController.token) +// oauth.post('/login', oauthController.token) +// oauth.get('/authorize', oauthController.authorize) + +// oauth.use((req, res) => res.sendStatus(404)) + +// oauth.use((err, req, res, next) => { +// const error_msg = err.toString() +// log.warn('[OAUTH USE] ' + error_msg) +// res.status(500).send(error_msg) +// }) + +// module.exports = oauth diff --git a/server/helpers.js b/server/helpers.js index 2d6865ba..a0fadb6f 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -48,7 +48,7 @@ domPurify.addHook('beforeSanitizeElements', node => { module.exports = { randomString(length = 12) { - const wishlist = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + const wishlist = '0123456789abcdefghijklmnopqrstuvwxyz' return Array.from(crypto.randomFillSync(new Uint32Array(length))) .map(x => wishlist[x % wishlist.length]) .join('') diff --git a/server/routes.js b/server/routes.js index bf4dba86..c61e6d27 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1,14 +1,15 @@ const express = require('express') -const cookieParser = require('cookie-parser') const app = express() const initialize = require('./initialize.server') const config = require('./config') const helpers = require('./helpers') -app.use(helpers.initSettings) -app.use(helpers.logRequest) -app.use(helpers.serveStatic()) -app.use(cookieParser()) + +app.use([ + helpers.initSettings, + helpers.logRequest, + helpers.serveStatic() +]) async function main () { @@ -18,7 +19,6 @@ async function main () { // const promBundle = require('express-prom-bundle') // const metricsMiddleware = promBundle({ includeMethod: true }) - const log = require('./log') const api = require('./api') @@ -28,14 +28,13 @@ async function main () { if (config.status === 'READY') { const cors = require('cors') const { spamFilter } = require('./federation/helpers') - const oauth = require('./api/oauth') - const auth = require('./api/auth') const federation = require('./federation') const webfinger = require('./federation/webfinger') const exportController = require('./api/controller/export') const tagController = require('./api/controller/tag') const placeController = require('./api/controller/place') const collectionController = require('./api/controller/collection') + const authController = require('./api/controller/oauth') // rss / ics feed app.use(helpers.feedRedirect) @@ -43,7 +42,6 @@ async function main () { app.get('/feed/:format/place/:placeName', cors(), placeController.getEvents) app.get('/feed/:format/collection/:name', cors(), collectionController.getEvents) app.get('/feed/:format', cors(), exportController.export) - app.use('/event/:slug', helpers.APRedirect) @@ -54,11 +52,11 @@ async function main () { // ignore unimplemented ping url from fediverse app.use(spamFilter) - // fill res.locals.user if request is authenticated - app.use(auth.fillUser) - - app.use('/oauth', oauth) - // app.use(metricsMiddleware) + app.use(authController.authenticate) + app.post('/oauth/token', authController.token) + app.post('/oauth/login', authController.login) + app.get('/oauth/authorize', authController.authorization) + app.post('/oauth/authorize', authController.decision) } // api! @@ -89,6 +87,8 @@ if (process.env.NODE_ENV !== 'test') { main() } +// app.listen(13120) + module.exports = { main, handler: app, diff --git a/tests/app.test.js b/tests/app.test.js index 8c1e8a9f..9387975a 100644 --- a/tests/app.test.js +++ b/tests/app.test.js @@ -54,7 +54,8 @@ describe('Authentication / Authorization', () => { test('should not authenticate with wrong user/password', () => { return request(app).post('/oauth/login') .set('Content-Type', 'application/x-www-form-urlencoded') - .expect(500) + .send({ email: 'admin', password: 'wrong'}) + .expect(401) }) test('should register an admin as first user', async () => { diff --git a/webcomponents/src/GancioEvents.svelte b/webcomponents/src/GancioEvents.svelte index fdcd778a..58a3e60c 100644 --- a/webcomponents/src/GancioEvents.svelte +++ b/webcomponents/src/GancioEvents.svelte @@ -57,7 +57,7 @@ update() }) $: update( - maxlength && title && places && tags && theme && show_recurrent && sidebar + maxlength && title && places && tags && theme && show_recurrent && sidebar && baseurl ) diff --git a/wp-plugin/gancio.php b/wp-plugin/gancio.php index 9a5e79d3..bc598d4a 100644 --- a/wp-plugin/gancio.php +++ b/wp-plugin/gancio.php @@ -3,7 +3,7 @@ Plugin Name: WPGancio Plugin URI: https://gancio.org Description: Connects an user of a gancio instance to a Wordpress user so that published events are automatically pushed with Gancio API. -Version: 1.0 +Version: 1.4 Author: Gancio License: AGPL 3.0 @@ -20,9 +20,11 @@ along with (WPGancio). If not, see (https://www.gnu.org/licenses/agpl-3.0.html). */ defined( 'ABSPATH' ) or die( 'Nope, not accessing this' ); -require_once('settings.php'); -require_once('wc.php'); -require_once('oauth.php'); +define( 'WPGANCIO_DIR', plugin_dir_path( __FILE__ ) ); +require_once(WPGANCIO_DIR . 'settings.php'); +require_once(WPGANCIO_DIR . 'network_settings.php'); +require_once(WPGANCIO_DIR . 'wc.php'); +require_once(WPGANCIO_DIR . 'oauth.php'); /** diff --git a/wp-plugin/network_settings.php b/wp-plugin/network_settings.php new file mode 100644 index 00000000..847bbfd2 --- /dev/null +++ b/wp-plugin/network_settings.php @@ -0,0 +1,112 @@ +settings_slug . '-page-options' ); + + // function wpgancio_update_options ($old_value, $instance_url) { + $redirect_uri = network_admin_url('settings.php?page=wpgancio'); + $query = join('&', array( + 'response_type=code', + 'redirect_uri=' . esc_url($redirect_uri), + 'scope=event:write', + 'client_id=' . get_site_option('wpgancio_client_id'), + )); + + wp_redirect("${instance_url}/oauth/authorize?${query}"); + exit; +} + +function wpgancio_network_options_page () { + add_submenu_page('settings.php', 'Gancio', 'Gancio', 'manage_options', 'wpgancio', 'wpgancio_network_options_page_html'); +} + +// function wpgancio_options_page() { +// // add top level menu page +// add_options_page( +// 'Gancio', +// 'Gancio', +// 'manage_options', +// 'wpgancio', +// 'wpgancio_options_page_html' +// ); +// } + +// instance url field cb +// field callbacks can accept an $args parameter, which is an array. +// $args is defined at the add_settings_field() function. +// wordpress has magic interaction with the following keys: label_for, class. +// the "label_for" key value is used for the "for" attribute of the