diff --git a/.meteor/versions b/.meteor/versions index 8908839b5..ebd055bec 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -140,7 +140,7 @@ webapp-hashing@1.1.1 wekan-accounts-cas@0.1.0 wekan-accounts-lockout@1.1.0 wekan-accounts-oidc@1.0.10 -wekan-accounts-sandstorm@0.8.0 +wekan-accounts-sandstorm@0.9.0 wekan-fontawesome@6.4.2 wekan-fullcalendar@3.10.5 wekan-ldap@0.0.2 diff --git a/packages/wekan-accounts-sandstorm/client.js b/packages/wekan-accounts-sandstorm/client.js index 61c176fcb..5f3f208ea 100644 --- a/packages/wekan-accounts-sandstorm/client.js +++ b/packages/wekan-accounts-sandstorm/client.js @@ -86,7 +86,7 @@ function loginWithSandstorm(connection, apiHost, apiToken) { var sendXhr = function () { if (!waiting) return; // Method call finished. - headers = {"Content-Type": "application/x-sandstorm-login-token"}; + var headers = {"Content-Type": "application/x-sandstorm-login-token"}; var testInfo = localStorage.sandstormTestUserInfo; if (testInfo) { @@ -120,16 +120,20 @@ function loginWithSandstorm(connection, apiHost, apiToken) { // Send the token in an HTTP POST request which on the server side will allow us to receive the // Sandstorm headers. - HTTP.post(postUrl, - {content: token, headers: headers}, - function (error, result) { - if (error) { - console.error("couldn't get /.sandstorm-login:", error); + fetch(postUrl, { + method: 'POST', + headers: headers, + body: token + }).then(function (response) { + if (!response.ok) { + throw new Error(response.statusText); + } + }).catch(function (error) { + console.error("couldn't get /.sandstorm-login:", error); - if (waiting) { - // Try again in a second. - Meteor.setTimeout(sendXhr, 1000); - } + if (waiting) { + // Try again in a second. + Meteor.setTimeout(sendXhr, 1000); } }); }; diff --git a/packages/wekan-accounts-sandstorm/package.js b/packages/wekan-accounts-sandstorm/package.js index b3972324b..8e8a06f64 100644 --- a/packages/wekan-accounts-sandstorm/package.js +++ b/packages/wekan-accounts-sandstorm/package.js @@ -21,7 +21,7 @@ Package.describe({ summary: "Login service for Sandstorm.io applications", - version: "0.8.0", + version: "0.9.0", name: "wekan-accounts-sandstorm", git: "https://github.com/sandstorm-io/meteor-accounts-sandstorm.git" }); @@ -30,7 +30,7 @@ Package.onUse(function(api) { api.use('random', ['client', 'server']); api.use('accounts-base', ['client', 'server'], {weak: true}); api.use('webapp', 'server'); - api.use('http', 'client'); + api.use('fetch', 'client'); api.use('tracker', 'client'); api.use('reactive-var', 'client'); api.use('check', 'server'); diff --git a/packages/wekan-accounts-sandstorm/server.js b/packages/wekan-accounts-sandstorm/server.js index d1f781cb2..7de97c949 100644 --- a/packages/wekan-accounts-sandstorm/server.js +++ b/packages/wekan-accounts-sandstorm/server.js @@ -43,12 +43,6 @@ if (__meteor_runtime_config__.SANDSTORM) { }); } - var Future = Npm.require("fibers/future"); - - var inMeteor = Meteor.bindEnvironment(function (callback) { - callback(); - }); - var logins = {}; // Maps tokens to currently-waiting login method calls. @@ -83,22 +77,24 @@ if (__meteor_runtime_config__.SANDSTORM) { }); Meteor.methods({ - loginWithSandstorm: function (token) { + async loginWithSandstorm(token) { check(token, String); - var future = new Future(); + const loginPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Meteor.Error("timeout", "Gave up waiting for login rendezvous XHR.")); + }, 10000); - logins[token] = future; - - var timeout = setTimeout(function () { - future.throw(new Meteor.Error("timeout", "Gave up waiting for login rendezvous XHR.")); - }, 10000); + logins[token] = { resolve, reject, timeout }; + }); var info; try { - info = future.wait(); + info = await loginPromise; } finally { - clearTimeout(timeout); + if (logins[token] && logins[token].timeout) { + clearTimeout(logins[token].timeout); + } delete logins[token]; } @@ -128,85 +124,79 @@ if (__meteor_runtime_config__.SANDSTORM) { return next(); }); - function readAll(stream) { - var future = new Future(); - - var chunks = []; - stream.on("data", function (chunk) { - chunks.push(chunk.toString()); + async function readAll(stream) { + return new Promise((resolve, reject) => { + const chunks = []; + stream.on("data", function (chunk) { + chunks.push(chunk.toString()); + }); + stream.on("error", function (err) { + reject(err); + }); + stream.on("end", function () { + resolve(chunks.join("")); + }); }); - stream.on("error", function (err) { - future.throw(err); - }); - stream.on("end", function () { - future.return(); - }); - - future.wait(); - - return chunks.join(""); } - var handlePostToken = Meteor.bindEnvironment(function (req, res) { - inMeteor(function () { - try { - // Note that cross-origin POSTs cannot set arbitrary Content-Types without explicit CORS - // permission, so this effectively prevents XSRF. - if (req.headers["content-type"].split(";")[0].trim() !== "application/x-sandstorm-login-token") { - throw new Error("wrong Content-Type for .sandstorm-login: " + req.headers["content-type"]); - } - - var token = readAll(req); - - var future = logins[token]; - if (!future) { - throw new Error("no current login request matching token"); - } - - var permissions = req.headers["x-sandstorm-permissions"]; - if (permissions && permissions !== "") { - permissions = permissions.split(","); - } else { - permissions = []; - } - - var sandstormInfo = { - id: req.headers["x-sandstorm-user-id"] || null, - name: decodeURIComponent(req.headers["x-sandstorm-username"]), - permissions: permissions, - picture: req.headers["x-sandstorm-user-picture"] || null, - preferredHandle: req.headers["x-sandstorm-preferred-handle"] || null, - pronouns: req.headers["x-sandstorm-user-pronouns"] || null, - }; - - var userInfo = {sandstorm: sandstormInfo}; - if (Package["accounts-base"]) { - if (sandstormInfo.id) { - // The user is logged into Sandstorm. Create a Meteor account for them, or find the - // existing one, and record the user ID. - var login = Package["accounts-base"].Accounts.updateOrCreateUserFromExternalService( - "sandstorm", sandstormInfo, {profile: {name: sandstormInfo.name}}); - userInfo.userId = login.userId; - } else { - userInfo.userId = null; - } - } else { - // Since the app isn't using regular Meteor accounts, we can define Meteor.userId() - // however we want. - userInfo.userId = sandstormInfo.id; - } - - userInfo.sessionId = req.headers["x-sandstorm-session-id"] || null; - userInfo.tabId = req.headers["x-sandstorm-tab-id"] || null; - future.return(userInfo); - res.writeHead(204, {}); - res.end(); - } catch (err) { - res.writeHead(500, { - "Content-Type": "text/plain" - }); - res.end(err.stack); + var handlePostToken = Meteor.bindEnvironment(async function (req, res) { + try { + // Note that cross-origin POSTs cannot set arbitrary Content-Types without explicit CORS + // permission, so this effectively prevents XSRF. + if (req.headers["content-type"].split(";")[0].trim() !== "application/x-sandstorm-login-token") { + throw new Error("wrong Content-Type for .sandstorm-login: " + req.headers["content-type"]); } - }); + + var token = await readAll(req); + + var loginEntry = logins[token]; + if (!loginEntry) { + throw new Error("no current login request matching token"); + } + + var permissions = req.headers["x-sandstorm-permissions"]; + if (permissions && permissions !== "") { + permissions = permissions.split(","); + } else { + permissions = []; + } + + var sandstormInfo = { + id: req.headers["x-sandstorm-user-id"] || null, + name: decodeURIComponent(req.headers["x-sandstorm-username"]), + permissions: permissions, + picture: req.headers["x-sandstorm-user-picture"] || null, + preferredHandle: req.headers["x-sandstorm-preferred-handle"] || null, + pronouns: req.headers["x-sandstorm-user-pronouns"] || null, + }; + + var userInfo = {sandstorm: sandstormInfo}; + if (Package["accounts-base"]) { + if (sandstormInfo.id) { + // The user is logged into Sandstorm. Create a Meteor account for them, or find the + // existing one, and record the user ID. + var login = await Package["accounts-base"].Accounts.updateOrCreateUserFromExternalService( + "sandstorm", sandstormInfo, {profile: {name: sandstormInfo.name}}); + userInfo.userId = login.userId; + } else { + userInfo.userId = null; + } + } else { + // Since the app isn't using regular Meteor accounts, we can define Meteor.userId() + // however we want. + userInfo.userId = sandstormInfo.id; + } + + userInfo.sessionId = req.headers["x-sandstorm-session-id"] || null; + userInfo.tabId = req.headers["x-sandstorm-tab-id"] || null; + loginEntry.resolve(userInfo); + res.writeHead(204, {}); + res.end(); + } catch (err) { + res.writeHead(500, { + "Content-Type": "text/plain" + }); + res.end(err.stack); + } }); } diff --git a/sandstorm.js b/sandstorm.js index 81d8c2cc9..2d2c79b28 100644 --- a/sandstorm.js +++ b/sandstorm.js @@ -51,7 +51,7 @@ if (isSandstorm && Meteor.isServer) { } Meteor.methods({ - sandstormClaimIdentityRequest(token, descriptor) { + async sandstormClaimIdentityRequest(token, descriptor) { check(token, String); check(descriptor, String); @@ -79,91 +79,68 @@ if (isSandstorm && Meteor.isServer) { const session = httpBridge.getSessionContext(sessionId).context; const api = httpBridge.getSandstormApi(sessionId).api; - Meteor.wrapAsync(done => { - session - .claimRequest(token) - .then(response => { - const identity = response.cap.castAs(Identity.Identity); - const promises = [ - api.getIdentityId(identity), - identity.getProfile(), - httpBridge.saveIdentity(identity), - ]; - return Promise.all(promises).then(responses => { - const identityId = responses[0].id.toString('hex').slice(0, 32); - const profile = responses[1].profile; - return profile.picture.getUrl().then(response => { - const sandstormInfo = { - id: identityId, - name: profile.displayName.defaultText, - permissions, - picture: `${response.protocol}://${response.hostPath}`, - preferredHandle: profile.preferredHandle, - pronouns: profile.pronouns, - }; + const response = await session.claimRequest(token); + const identity = response.cap.castAs(Identity.Identity); + const [identityIdResult, profileResult] = await Promise.all([ + api.getIdentityId(identity), + identity.getProfile(), + httpBridge.saveIdentity(identity), + ]); + const identityId = identityIdResult.id.toString('hex').slice(0, 32); + const profile = profileResult.profile; + const pictureResponse = await profile.picture.getUrl(); + const sandstormInfo = { + id: identityId, + name: profile.displayName.defaultText, + permissions, + picture: `${pictureResponse.protocol}://${pictureResponse.hostPath}`, + preferredHandle: profile.preferredHandle, + pronouns: profile.pronouns, + }; - const login = Accounts.updateOrCreateUserFromExternalService( - 'sandstorm', - sandstormInfo, - { profile: { name: sandstormInfo.name } }, - ); + const login = await Accounts.updateOrCreateUserFromExternalService( + 'sandstorm', + sandstormInfo, + { profile: { name: sandstormInfo.name } }, + ); - updateUserPermissions(login.userId, permissions); - done(); - }); - }); - }) - .catch(e => { - done(e, null); - }); - })(); + await updateUserPermissions(login.userId, permissions); }, }); - function reportActivity(sessionId, path, type, users, caption) { + async function reportActivity(sessionId, path, type, users, caption) { const httpBridge = getHttpBridge(); const session = httpBridge.getSessionContext(sessionId).context; - Meteor.wrapAsync(done => { - return Promise.all( - users.map(user => { - return httpBridge - .getSavedIdentity(user.id) - .then(response => { - // Call getProfile() to make sure that the identity successfully resolves. - // (In C++ we would instead call whenResolved() here.) - const identity = response.identity; - return identity.getProfile().then(() => { - return { - identity, - mentioned: !!user.mentioned, - subscribed: !!user.subscribed, - }; - }); - }) - .catch(() => { - // Ignore identities that fail to restore. Either they were added before we set - // `saveIdentityCaps` to true, or they have lost access to the board. - }); - }), - ) - .then(maybeUsers => { - const users = maybeUsers.filter(u => !!u); - const event = { path, type, users }; - if (caption) { - event.notification = { caption }; - } - - return session.activity(event); - }) - .then( - () => done(), - e => done(e), - ); - })(); + const maybeUsers = await Promise.all( + users.map(async (user) => { + try { + const response = await httpBridge.getSavedIdentity(user.id); + // Call getProfile() to make sure that the identity successfully resolves. + // (In C++ we would instead call whenResolved() here.) + const identity = response.identity; + await identity.getProfile(); + return { + identity, + mentioned: !!user.mentioned, + subscribed: !!user.subscribed, + }; + } catch (e) { + // Ignore identities that fail to restore. Either they were added before we set + // `saveIdentityCaps` to true, or they have lost access to the board. + return undefined; + } + }), + ); + const resolvedUsers = maybeUsers.filter(u => !!u); + const event = { path, type, users: resolvedUsers }; + if (caption) { + event.notification = { caption }; + } + await session.activity(event); } Meteor.startup(() => { - Activities.after.insert((userId, doc) => { + Activities.after.insert(async (userId, doc) => { // HACK: We need the connection that's making the request in order to read the // Sandstorm session ID. const invocation = DDP._CurrentInvocation.get(); // eslint-disable-line no-undef @@ -177,9 +154,9 @@ if (isSandstorm && Meteor.isServer) { ); if (defIdx >= 0) { const users = {}; - function ensureUserListed(userId) { + async function ensureUserListed(userId) { if (!users[userId]) { - const user = Meteor.users.findOne(userId); + const user = await Meteor.users.findOneAsync(userId); if (user) { users[userId] = { id: user.services.sandstorm.id }; } else { @@ -189,14 +166,14 @@ if (isSandstorm && Meteor.isServer) { return true; } - function mentionedUser(userId) { - if (ensureUserListed(userId)) { + async function mentionedUser(userId) { + if (await ensureUserListed(userId)) { users[userId].mentioned = true; } } - function subscribedUser(userId) { - if (ensureUserListed(userId)) { + async function subscribedUser(userId) { + if (await ensureUserListed(userId)) { users[userId].subscribed = true; } } @@ -206,11 +183,16 @@ if (isSandstorm && Meteor.isServer) { if (doc.cardId) { path = `b/sandstorm/libreboard/${doc.cardId}`; - ReactiveCache.getCard(doc.cardId).members.map(subscribedUser); + const card = ReactiveCache.getCard(doc.cardId); + if (card && card.members) { + for (const memberId of card.members) { + await subscribedUser(memberId); + } + } } if (doc.memberId) { - mentionedUser(doc.memberId); + await mentionedUser(doc.memberId); } if (doc.activityType === 'addComment') { @@ -220,23 +202,24 @@ if (isSandstorm && Meteor.isServer) { ReactiveCache.getBoard(sandstormBoard._id).activeMembers(), 'userId', ); - (comment.text.match(/\B@([\w.]*)/g) || []).forEach(username => { - const user = Meteor.users.findOne({ + const mentions = comment.text.match(/\B@([\w.]*)/g) || []; + for (const username of mentions) { + const user = await Meteor.users.findOneAsync({ username: username.slice(1), }); if (user && activeMembers.indexOf(user._id) !== -1) { - mentionedUser(user._id); + await mentionedUser(user._id); } - }); + } } - reportActivity(sessionId, path, defIdx, _.values(users), caption); + await reportActivity(sessionId, path, defIdx, _.values(users), caption); } } }); }); - function updateUserPermissions(userId, permissions) { + async function updateUserPermissions(userId, permissions) { const isActive = permissions.indexOf('participate') > -1; const isAdmin = permissions.indexOf('configure') > -1; const isCommentOnly = false; @@ -260,7 +243,7 @@ if (isSandstorm && Meteor.isServer) { else if (!isActive) modifier = {}; else modifier = { $push: { members: permissionDoc } }; - Boards.update(sandstormBoard._id, modifier); + await Boards.updateAsync(sandstormBoard._id, modifier); } Picker.route('/', (params, req, res) => { @@ -288,14 +271,14 @@ if (isSandstorm && Meteor.isServer) { // unique board document. Note that when the `Users.after.insert` hook is // called, the user is inserted into the database but not connected. So // despite the appearances `userId` is null in this block. - Users.after.insert((userId, doc) => { + Users.after.insert(async (userId, doc) => { if (!ReactiveCache.getBoard(sandstormBoard._id)) { - Boards.insert(sandstormBoard, { validate: false }); - Swimlanes.insert({ + await Boards.insertAsync(sandstormBoard, { validate: false }); + await Swimlanes.insertAsync({ title: 'Default', boardId: sandstormBoard._id, }); - Activities.update( + await Activities.updateAsync( { activityTypeId: sandstormBoard._id }, { $set: { userId: doc._id } }, ); @@ -313,7 +296,7 @@ if (isSandstorm && Meteor.isServer) { const username = doc.services.sandstorm.preferredHandle; let appendNumber = 0; while ( - ReactiveCache.getUser({ + await Meteor.users.findOneAsync({ _id: { $ne: doc._id }, username: generateUniqueUsername(username, appendNumber), }) @@ -321,7 +304,7 @@ if (isSandstorm && Meteor.isServer) { appendNumber += 1; } - Users.update(doc._id, { + await Users.updateAsync(doc._id, { $set: { username: generateUniqueUsername(username, appendNumber), 'profile.fullname': doc.services.sandstorm.name, @@ -329,27 +312,27 @@ if (isSandstorm && Meteor.isServer) { }, }); - updateUserPermissions(doc._id, doc.services.sandstorm.permissions); + await updateUserPermissions(doc._id, doc.services.sandstorm.permissions); }); - Meteor.startup(() => { - Users.find().observeChanges({ - changed(userId, fields) { + Meteor.startup(async () => { + await Users.find().observeChangesAsync({ + async changed(userId, fields) { const sandstormData = (fields.services || {}).sandstorm || {}; if (sandstormData.name) { - Users.update(userId, { + await Users.updateAsync(userId, { $set: { 'profile.fullname': sandstormData.name }, }); } if (sandstormData.picture) { - Users.update(userId, { + await Users.updateAsync(userId, { $set: { 'profile.avatarUrl': sandstormData.picture }, }); } if (sandstormData.permissions) { - updateUserPermissions(userId, sandstormData.permissions); + await updateUserPermissions(userId, sandstormData.permissions); } }, }); @@ -373,9 +356,9 @@ if (isSandstorm && Meteor.isServer) { HTTP.methods = newMethods => { Object.keys(newMethods).forEach(key => { if (newMethods[key].auth) { - newMethods[key].auth = function() { + newMethods[key].auth = async function() { const sandstormID = this.req.headers['x-sandstorm-user-id']; - const user = Meteor.users.findOne({ + const user = await Meteor.users.findOneAsync({ 'services.sandstorm.id': sandstormID, }); return user && user._id;