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 @@ - + \ 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