diff --git a/CHANGELOG b/CHANGELOG
index 12056700..17836041 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,12 +1,14 @@
All notable changes to this project will be documented in this file.
### UNRELEASED
- - new plugin system
+ - new plugin system - fix #17
- new "publish on telegram" plugin: thanks @fadelkon
- i18n refactoring
- people can now choose the language displayed - fix #171
- fix place "[Object]" issue - #194
- admin could choose a custom fallback image - fix #195
+ - it is now possible NOT to enter the end time of an event - fix #188
+
### 1.5.6 - 22 set '22
diff --git a/nuxt.config.js b/nuxt.config.js
index 18e28ab2..5663babc 100644
--- a/nuxt.config.js
+++ b/nuxt.config.js
@@ -125,7 +125,7 @@ module.exports = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
},
logout: false,
- user: { url: '/user', method: 'get', propertyName: false }
+ user: { url: '/user', method: 'get', propertyName: false, autoFetch: false }
},
tokenRequired: true,
tokenType: 'Bearer'
diff --git a/package.json b/package.json
index 6dcf96e2..a0339c26 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
"dompurify": "^2.3.10",
"email-templates": "^10.0.1",
"express": "^4.18.1",
- "express-oauth-server": "lesion/express-oauth-server#master",
+ "express-session": "^1.17.3",
"http-signature": "^1.3.6",
"https-proxy-agent": "^5.0.1",
"ical.js": "^1.5.0",
@@ -65,6 +65,14 @@
"mysql2": "^2.3.3",
"nuxt-edge": "2.16.0-27720022.54e852f",
"nuxt-i18n": "^6.28.1",
+ "oauth2orize": "^1.11.1",
+ "passport": "^0.6.0",
+ "passport-anonymous": "^1.0.1",
+ "passport-custom": "^1.1.1",
+ "passport-http": "^0.3.0",
+ "passport-http-bearer": "^1.0.1",
+ "passport-oauth2-client-password": "^0.1.2",
+ "passport-oauth2-client-public": "^0.0.1",
"pg": "^8.8.0",
"sequelize": "^6.23.0",
"sequelize-slugify": "^1.6.2",
@@ -94,7 +102,6 @@
},
"resolutions": {
"nth-check": "^2.0.1",
- "express-oauth-server/**/lodash": "^4.17.21",
"glob-parent": "^5.1.2",
"moment": "^2.29.2"
},
diff --git a/pages/Authorize.vue b/pages/Authorize.vue
index 4b3a9614..d8c0dfe3 100644
--- a/pages/Authorize.vue
+++ b/pages/Authorize.vue
@@ -1,18 +1,21 @@
-.d-flex.justify-space-around
+v-form.d-flex.justify-space-around(method='post' action='/oauth/authorize')
v-card.mt-5(max-width='600px')
v-card-title {{settings.title}} - {{$t('common.authorize')}}
v-card-text
+ h2 {{$auth.user.email}}
+ input(name='transaction_id' :value='transactionID' type='hidden')
u {{$auth.user.email}}
+
div
- p(v-html="$t('oauth.authorization_request', { app: client.name, instance_name: settings.title })")
+ p(v-html="$t('oauth.authorization_request', { app: client, instance_name: settings.title })")
ul.mb-2
- li(v-for="s in scope.split(' ')") {{$t(`oauth.scopes.${scope}`)}}
- span(v-html="$t('oauth.redirected_to', {url: $route.query.redirect_uri})")
+ li {{$t(`oauth.scopes.${scope}`)}}
+ span(v-html="$t('oauth.redirected_to', {url: redirect_uri})")
v-card-actions
v-spacer
- v-btn(color='error' to='/') {{$t('common.cancel')}}
- v-btn(:href='authorizeURL' color='success') {{$t('common.authorize')}}
+ v-btn(color='error' to='/' outlined) {{$t('common.cancel')}}
+ v-btn(type='submit' color='success' outlined) {{$t('common.authorize')}}
-
+
\ 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