Merge pull request #6112 from harryadel/wekan-accounts-sandstorm-async-migration

Migrate wekan-accounts-sandstorm to async API for Meteor 3.0
This commit is contained in:
Lauri Ojansivu 2026-02-13 07:18:57 +02:00 committed by GitHub
commit 66ccba9267
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 187 additions and 210 deletions

View file

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

View file

@ -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);
}
});
};

View file

@ -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');

View file

@ -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);
}
});
}

View file

@ -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;