浏览代码

Merge remote-tracking branch 'origin/upgrade-meteor' into upgrade-meteor

Martin Filser 3 年之前
父节点
当前提交
1fc3ed407a

+ 1 - 1
.meteor/packages

@@ -26,7 +26,6 @@ mongo@1.15.0-rc272.0
 mquandalle:collection-mutations
 
 # Account system
-kenton:accounts-sandstorm
 #wekan-ldap
 #wekan-accounts-cas
 #wekan-accounts-oidc
@@ -142,3 +141,4 @@ useraccounts:unstyled
 service-configuration@1.3.0
 communitypackages:picker
 simple:rest-accounts-password
+wekan-accounts-sandstorm

+ 1 - 1
.meteor/versions

@@ -76,7 +76,6 @@ jquery@1.11.11
 kadira:blaze-layout@2.3.0
 kadira:dochead@1.5.0
 kadira:flow-router@2.12.1
-kenton:accounts-sandstorm@0.1.0
 konecty:mongo-counter@0.0.5_3
 launch-screen@1.3.0
 livedata@1.0.18
@@ -225,5 +224,6 @@ useraccounts:flow-routing@1.15.0
 useraccounts:unstyled@1.14.2
 webapp@1.13.1
 webapp-hashing@1.1.0
+wekan-accounts-sandstorm@0.7.0
 wekan-markdown@1.0.9
 zimme:active-route@2.3.2

+ 1 - 2
client/components/boards/boardsList.js

@@ -260,7 +260,7 @@ BlazeComponent.extendComponent({
             },
             (err, res) => {
               if (err) {
-                self.setError(err.error);
+                console.error(err);
               } else {
                 Session.set('fromBoard', null);
                 subManager.subscribe('board', res, false);
@@ -268,7 +268,6 @@ BlazeComponent.extendComponent({
                   id: res,
                   slug: title,
                 });
-                //Utils.goBoardId(res);
               }
             },
           );

+ 2 - 1
models/attachments.js

@@ -34,12 +34,13 @@ Attachments = new FilesCollection({
     return ret;
   },
   onAfterUpload(fileObj) {
+    let storage = fileObj.meta.copyStorage || STORAGE_NAME_GRIDFS;
     // current storage is the filesystem, update object and database
     Object.keys(fileObj.versions).forEach(versionName => {
       fileObj.versions[versionName].storage = STORAGE_NAME_FILESYSTEM;
     });
     Attachments.update({ _id: fileObj._id }, { $set: { "versions" : fileObj.versions } });
-    moveToStorage(fileObj, STORAGE_NAME_GRIDFS, fileStoreStrategyFactory);
+    moveToStorage(fileObj, storage, fileStoreStrategyFactory);
   },
   interceptDownload(http, fileObj, versionName) {
     const ret = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName).interceptDownload(http, this.cacheControl);

+ 7 - 6
models/cards.js

@@ -5,7 +5,8 @@ import {
   TYPE_LINKED_BOARD,
   TYPE_LINKED_CARD,
 } from '../config/const';
-import Attachments from "./attachments";
+import Attachments, { fileStoreStrategyFactory } from "./attachments";
+import { copyFile } from './lib/fileStoreStrategy.js';
 
 
 Cards = new Mongo.Collection('cards');
@@ -586,11 +587,11 @@ Cards.helpers({
     const _id = Cards.insert(this);
 
     // Copy attachments
-    oldCard.attachments().forEach(att => {
-      att.cardId = _id;
-      delete att._id;
-      return Attachments.insert(att);
-    });
+    oldCard.attachments()
+      .map(att => att.get())
+      .forEach(att => {
+        copyFile(att, _id, fileStoreStrategyFactory);
+      });
 
     // copy checklists
     Checklists.find({ cardId: oldId }).forEach(ch => {

+ 55 - 2
models/lib/fileStoreStrategy.js

@@ -312,11 +312,11 @@ export const moveToStorage = function(fileObj, storageDestination, fileStoreStra
       const writeStream = strategyWrite.getWriteStream(filePath);
 
       writeStream.on('error', error => {
-        console.error('[writeStream error]: ', error, fileObjId);
+        console.error('[writeStream error]: ', error, fileObj._id);
       });
 
       readStream.on('error', error => {
-        console.error('[readStream error]: ', error, fileObjId);
+        console.error('[readStream error]: ', error, fileObj._id);
       });
 
       writeStream.on('finish', Meteor.bindEnvironment((finishedData) => {
@@ -336,3 +336,56 @@ export const moveToStorage = function(fileObj, storageDestination, fileStoreStra
     }
   });
 };
+
+export const copyFile = function(fileObj, newCardId, fileStoreStrategyFactory) {
+  const versionName = "original";
+  const strategyRead = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName);
+  const readStream = strategyRead.getReadStream();
+  const strategyWrite = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName, STORAGE_NAME_FILESYSTEM);
+
+  const tempPath = path.join(fileStoreStrategyFactory.storagePath, Random.id() + "-" + versionName + "-" + fileObj.name);
+  const writeStream = strategyWrite.getWriteStream(tempPath);
+
+  writeStream.on('error', error => {
+    console.error('[writeStream error]: ', error, fileObj._id);
+  });
+
+  readStream.on('error', error => {
+    console.error('[readStream error]: ', error, fileObj._id);
+  });
+
+  // https://forums.meteor.com/t/meteor-code-must-always-run-within-a-fiber-try-wrapping-callbacks-that-you-pass-to-non-meteor-libraries-with-meteor-bindenvironmen/40099/8
+  readStream.on('end', Meteor.bindEnvironment(() => {
+    const fileId = Random.id();
+    Attachments.addFile(
+      tempPath,
+      {
+        fileName: fileObj.name,
+        type: fileObj.type,
+        meta: {
+          boardId: fileObj.meta.boardId,
+          cardId: newCardId,
+          listId: fileObj.meta.listId,
+          swimlaneId: fileObj.meta.swimlaneId,
+          source: 'copy',
+          copyFrom: fileObj._id,
+          copyStorage: strategyRead.getStorageName(),
+        },
+        userId: fileObj.userId,
+        size: fileObj.fileSize,
+        fileId,
+      },
+      (err, fileRef) => {
+        if (err) {
+          console.log(err);
+        } else {
+          // Set the userId again
+          Attachments.update({ _id: fileRef._id }, { $set: { userId: fileObj.userId } });
+        }
+      },
+      true,
+    );
+  }));
+
+  readStream.pipe(writeStream);
+};

+ 3 - 0
packages/wekan-accounts-sandstorm/.gitignore

@@ -0,0 +1,3 @@
+.build*
+test-app/.meteor/local
+test-app/.meteor-spk

+ 17 - 0
packages/wekan-accounts-sandstorm/.test-app/.meteor/.finished-upgraders

@@ -0,0 +1,17 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
+notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
+1.3.0-split-minifiers-package
+1.4.0-remove-old-dev-bundle-link
+1.4.1-add-shell-server-package
+1.4.3-split-account-service-packages
+1.5-add-dynamic-import-package

+ 1 - 0
packages/wekan-accounts-sandstorm/.test-app/.meteor/.gitignore

@@ -0,0 +1 @@
+local

+ 7 - 0
packages/wekan-accounts-sandstorm/.test-app/.meteor/.id

@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+#   - ensuring you don't accidentally deploy one app on top of another
+#   - providing package authors with aggregated statistics
+
+1w4v0yxh077n01wrnl8j

+ 31 - 0
packages/wekan-accounts-sandstorm/.test-app/.meteor/packages

@@ -0,0 +1,31 @@
+# Meteor packages used by this project, one per line.
+# Check this file (and the other files in this directory) into your repository.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+# List accounts-sandstorm first so that any missing dependencies it has
+# are discovered.
+kenton:accounts-sandstorm
+
+# Optional dependency. Should still work commented-out.
+accounts-base@1.3.1
+
+meteor-base@1.1.0             # Packages every Meteor app needs to have
+mobile-experience@1.0.4       # Packages for a great mobile UX
+mongo@1.1.19                   # The database Meteor supports right now
+blaze-html-templates    # Compile .html files into Meteor Blaze views
+session@1.1.7                 # Client-side reactive dictionary for your app
+jquery@1.11.10                  # Helpful client-side library
+tracker@1.1.3                 # Meteor's client-side reactive programming library
+
+es5-shim@4.6.15                # ECMAScript 5 compatibility for older browsers.
+ecmascript@0.8.1              # Enable ECMAScript2015+ syntax in app code
+
+autopublish@1.0.7             # Publish all data to the clients (for prototyping)
+insecure@1.0.7                # Allow all DB writes from clients (for prototyping)
+
+standard-minifier-css
+standard-minifier-js
+shell-server
+dynamic-import

+ 2 - 0
packages/wekan-accounts-sandstorm/.test-app/.meteor/platforms

@@ -0,0 +1,2 @@
+server
+browser

+ 1 - 0
packages/wekan-accounts-sandstorm/.test-app/.meteor/release

@@ -0,0 +1 @@
+METEOR@1.5.1

+ 83 - 0
packages/wekan-accounts-sandstorm/.test-app/.meteor/versions

@@ -0,0 +1,83 @@
+accounts-base@1.3.1
+allow-deny@1.0.6
+autopublish@1.0.7
+autoupdate@1.3.12
+babel-compiler@6.19.4
+babel-runtime@1.0.1
+base64@1.0.10
+binary-heap@1.0.10
+blaze@2.3.2
+blaze-html-templates@1.1.2
+blaze-tools@1.0.10
+boilerplate-generator@1.1.1
+caching-compiler@1.1.9
+caching-html-compiler@1.1.2
+callback-hook@1.0.10
+check@1.2.5
+ddp@1.3.0
+ddp-client@2.0.0
+ddp-common@1.2.9
+ddp-rate-limiter@1.0.7
+ddp-server@2.0.0
+deps@1.0.12
+diff-sequence@1.0.7
+dynamic-import@0.1.1
+ecmascript@0.8.2
+ecmascript-runtime@0.4.1
+ecmascript-runtime-client@0.4.3
+ecmascript-runtime-server@0.4.1
+ejson@1.0.13
+es5-shim@4.6.15
+fastclick@1.0.13
+geojson-utils@1.0.10
+hot-code-push@1.0.4
+html-tools@1.0.11
+htmljs@1.0.11
+http@1.2.12
+id-map@1.0.9
+insecure@1.0.7
+jquery@1.11.10
+kenton:accounts-sandstorm@0.7.0
+launch-screen@1.1.1
+livedata@1.0.18
+localstorage@1.1.1
+logging@1.1.17
+meteor@1.7.1
+meteor-base@1.1.0
+minifier-css@1.2.16
+minifier-js@2.1.1
+minimongo@1.2.1
+mobile-experience@1.0.4
+mobile-status-bar@1.0.14
+modules@0.9.4
+modules-runtime@0.8.0
+mongo@1.1.22
+mongo-id@1.0.6
+npm-mongo@2.2.30
+observe-sequence@1.0.16
+ordered-dict@1.0.9
+promise@0.8.9
+random@1.0.10
+rate-limit@1.0.8
+reactive-dict@1.1.9
+reactive-var@1.0.11
+reload@1.1.11
+retry@1.0.9
+routepolicy@1.0.12
+service-configuration@1.0.11
+session@1.1.7
+shell-server@0.2.4
+spacebars@1.0.15
+spacebars-compiler@1.1.3
+standard-minifier-css@1.3.4
+standard-minifier-js@2.1.1
+templating@1.3.2
+templating-compiler@1.3.2
+templating-runtime@1.3.2
+templating-tools@1.1.2
+tracker@1.1.3
+ui@1.0.13
+underscore@1.0.10
+url@1.1.0
+webapp@1.3.17
+webapp-hashing@1.0.9

+ 1 - 0
packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.css

@@ -0,0 +1 @@
+/* CSS declarations go here */

+ 19 - 0
packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.html

@@ -0,0 +1,19 @@
+<head>
+  <title>accounts-meteor-test</title>
+</head>
+
+<body>
+  <h1>Welcome to Meteor!</h1>
+
+  {{> hello}}
+</body>
+
+<template name="hello">
+  <p>Resubscribes: {{counter}}</p>
+  
+  <h2>server</h2>
+  <pre>{{serverInfo}}</pre>
+
+  <h2>client</h2>
+  <pre>{{clientInfo}}</pre>
+</template>

+ 48 - 0
packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.js

@@ -0,0 +1,48 @@
+if (Meteor.isClient) {
+  var Info = new Mongo.Collection("info");
+  var Counter = new Mongo.Collection("counter");
+  
+  Template.hello.onCreated(function () {
+    Meteor.subscribe("info");
+    Meteor.subscribe("counter");
+  });
+
+  Template.hello.helpers({
+    counter: function () {
+      if (!Template.instance().subscriptionsReady()) return "not ready";
+      return Counter.findOne("counter").counter;
+    },
+    
+    serverInfo: function () {
+      var obj = Info.findOne("info");
+      console.log("server", Meteor.loggingIn && Meteor.loggingIn(), obj);
+      return JSON.stringify(obj, null, 2);
+    },
+    
+    clientInfo: function () {
+      var obj = Meteor.sandstormUser();
+      console.log("client", Meteor.loggingIn && Meteor.loggingIn(), obj);
+      return JSON.stringify(obj, null, 2);
+    },
+  });
+}
+
+if (Meteor.isServer) {
+  Meteor.startup(function () {
+    // code to run on server at startup
+  });
+  
+  Meteor.publish("info", function () {
+    var user = Meteor.users && this.userId && Meteor.users.findOne(this.userId);
+    this.added("info", "info", {userId: this.userId, user: user, sandstormUser: this.connection.sandstormUser(),
+                                sessionId: this.connection.sandstormSessionId(),
+                                tabId: this.connection.sandstormTabId()});
+    this.ready();
+  });
+  
+  var counter = 0;
+  Meteor.publish("counter", function () {
+    this.added("counter", "counter", {counter: counter++});
+    this.ready();
+  });
+}

+ 1 - 0
packages/wekan-accounts-sandstorm/.test-app/packages/kenton:accounts-sandstorm

@@ -0,0 +1 @@
+../..

+ 74 - 0
packages/wekan-accounts-sandstorm/.test-app/sandstorm-pkgdef.capnp

@@ -0,0 +1,74 @@
+@0xb412d6a17c04e5cc;
+
+using Spk = import "/sandstorm/package.capnp";
+
+const pkgdef :Spk.PackageDefinition = (
+  id = "y49n7yrxk6p3ud1hkgeup1mah6f7a488nancvay7v6y1wxq78cn0",
+
+  manifest = (
+    appTitle = (defaultText = "Meteor Accounts Test App"),
+    appVersion = 0,
+    appMarketingVersion = (defaultText = "0.0.0"),
+    actions = [
+      ( title = (defaultText = "New Test"),
+        command = .myCommand
+      )
+    ],
+
+    continueCommand = .myCommand,
+  ),
+
+  sourceMap = (
+    searchPath = [
+      ( sourcePath = ".meteor-spk/deps" ),
+      ( sourcePath = ".meteor-spk/bundle" )
+    ]
+  ),
+
+  alwaysInclude = [ "." ],
+
+  bridgeConfig = (
+    viewInfo = (
+      permissions = [
+        (
+          name = "editor",  
+          title = (defaultText = "editor"),
+          description = (defaultText = "grants ability to modify data"),
+        ),
+        (
+          name = "commenter",
+          title = (defaultText = "commenter"),
+          description = (defaultText = "grants ability to modify data"),
+        ),
+      ],
+      roles = [
+        (
+          title = (defaultText = "editor"),
+          permissions  = [true, true],
+          verbPhrase = (defaultText = "can edit"),
+          description = (defaultText = "editors may view all site data and change settings."),
+        ),
+        (
+          title = (defaultText = "commenter"),
+          permissions  = [false, true],
+          verbPhrase = (defaultText = "can comment"),
+          description = (defaultText = "viewers may view what other users have written."),
+        ),
+        (
+          title = (defaultText = "viewer"),
+          permissions  = [false, false],
+          verbPhrase = (defaultText = "can view"),
+          description = (defaultText = "viewers may view what other users have written."),
+        ),
+      ],
+    ),
+  ),
+);
+
+const myCommand :Spk.Manifest.Command = (
+  argv = ["/sandstorm-http-bridge", "4000", "--", "node", "start.js"],
+  environ = [
+    (key = "PATH", value = "/usr/local/bin:/usr/bin:/bin"),
+    (key = "SANDSTORM", value = "1"),
+  ]
+);

+ 21 - 0
packages/wekan-accounts-sandstorm/LICENSE

@@ -0,0 +1,21 @@
+Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors
+Licensed under the MIT License:
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+

+ 137 - 0
packages/wekan-accounts-sandstorm/README.md

@@ -0,0 +1,137 @@
+# Sandstorm.io login integration for Meteor.js
+
+[Sandstorm](https://sandstorm.io) is a platform for personal clouds that makes
+installing apps to your personal server as easy as installing apps to your
+phone.
+
+[Meteor](https://meteor.com) is a revolutionary web app framework. Sandstorm's
+own UI is built using Meteor, and Meteor is also a great way to build Sandstorm
+apps.
+
+This package is meant to be used by Meteor apps built to run on Sandstorm.
+It integrates with Sandstorm's built-in login system to log the user in
+automatically when they open the app. The user's `profile.name` will be
+populated from Sandstorm. When using this package, you should not use
+`accounts-ui` at all; just let login happen automatically.
+
+## Including in your app
+
+To use this package in your Meteor project, simply install it from the Meteor
+package repository:
+
+    meteor add kenton:accounts-sandstorm
+
+To package a Meteor app for Sandstorm, see
+[the Meteor app packaging guide](https://docs.sandstorm.io/en/latest/vagrant-spk/packaging-tutorial-meteor/).
+
+Note that this package does nothing if the `SANDSTORM` environment variable is
+not set. Therefore, it is safe to include the package even in non-Sandstorm
+builds of your app. Note that `sandstorm-pkgdef.capnp` files generated by
+`spk init` automatically have a line like `(key = "SANDSTORM", value = "1"),`
+which sets the environment variable, so you shouldn't have to do anything
+special to enable it.
+
+Conversely, when `SANDSTORM` is set, this package will enter Highlander Mode
+in which it will *disable* all other accounts packages. This makes it safe
+to include those other accounts packages in the Sandstorm build, which is
+often convenient, although they will add bloat to your spk.
+
+## Usage
+
+* On the client, call `Meteor.sandstormUser()`. (This is a reactive data source.)
+* In a method or publish (on the server), call `this.connection.sandstormUser()`.
+
+Either of these will return an object containing the following fields:
+
+* `id`: From `X-Sandstorm-User-Id`; globally unique and stable
+  identifier for this user. `null` if the user is not logged in.
+* `name`: From "X-Sandstorm-Username`, the user's display name (e.g.
+  `"Kenton Varda"`).
+* `picture`: From `X-Sandstorm-User-Picture`, URL of the user's preferred
+  avatar, or `null` if they don't have one.
+* `permissions`: From `X-Sandstorm-Permissions` (but parsed to a list),
+  the list of permissions the user has as determined by the Sandstorm
+  sharing model. Apps can define their own permissions.
+* `preferredHandle`: From `X-Sandstorm-Preferred-Handle`, the user's
+  preferred handle ("username", in the unix sense). This is NOT
+  guaranteed to be unique; it's just a different form of display name.
+* `pronouns`: From `X-Sandstorm-User-Pronouns`, indicates the pronouns
+  by which the user prefers to be referred.
+
+See [the Sandstorm docs](https://docs.sandstorm.io/en/latest/developing/auth/#headers-that-an-app-receives) for more information about these fields.
+
+Note that `sandstormUser()` may return `null` on the client side if the login
+handshake has not completed yet (`Meteor.loggingIn()` returns `true` during
+this time). It never returns `null` on the server, but it may throw an
+exception if the client skipped the authentication handshake (which indicates
+the client is not running accounts-sandstorm, which is rather suspicious!).
+
+## Synchronization with Meteor Accounts
+
+`accounts-sandstorm` does NOT require `accounts-base`. However, if you do
+include `accounts-base` in your dependencies, then `accounts-sandstorm` will
+integrate with it in order to store information about users seen previously.
+In particular:
+
+* A Meteor account will be automatically created for each logged-in Sandstorm user,
+  the first time they visit the grain.
+* In the `Meteor.users` table, `services.sandstorm` will contain the same data
+  returned by `Meteor.sandstormUser()`.
+* `Meteor.loggingIn()` will return `true` during the initial handshake (when
+  `sandstormUser()` would return `null`).
+
+Please note, however, that you should prefer `sandstormUser()` over
+`Meteor.user().services.sandstorm` whenever possible, **especially** for enforcing
+permissions, for a few reasons:
+
+* Anonymous users do NOT get a table entry, therefore `Meteor.user()` will be
+  `null` for them. However, anonymous users of a sharing link may have permissions!
+* Moreover, in the future, anonymous users may additionally be able to assign
+  themselves names, handles, avatars, etc. The only thing that makes them "anonymous"
+  is that they have not provided the app with a unique identifier that could be used
+  to recognize the same user when they visit again later.
+* `services.sandstorm` is only updated when the user is online; it may be stale
+  when they are not present. This implies that when a user's access is revoked,
+  their user table entry will never be updated again, and will continue to
+  indicate that they have permissions when they in fact no longer do.
+
+## Development aids
+
+`accounts-sandstorm` normally works its magic when running inside Sandstorm. However,
+it's often a lot more convenient to develop Meteor apps using Meteor's normal dev tools
+which currently cannot run inside Sandstorm.
+
+Therefore, when *not* running inside Sansdtorm, you may use the following console
+function to fake your user information:
+
+    SandstormAccounts.setTestUserInfo({
+      id: "12345",
+      name: "Alice",
+      // ... other parameters, as listed above ...
+    });
+
+This will cause `accounts-sandstorm` to spoof the `X-Sandstorm-*` headers with the
+parameters you provided when it attempts to log in. When actually running inside
+Sandstorm, such spoofing is blocked by Sandstorm, but when running outside it will
+work and now you can test your app.
+
+Note that this functionality, like all of the package, is only enabled if you set the
+`SANDSTORM` environment variable. So, run `meteor` like so:
+
+    SANDSTORM=1 meteor
+
+## Migrating from 0.1
+
+In version 0.1.x of this puackage, there was no `sandstormUser()` function; the
+only mode of operation was through Meteor accounts. This had problems with
+permissions and anonymous users as described adove. Introducing `sandstormUser()`
+is a huge update.
+
+For almost all users, 0.2 should be a drop-in replacement for 0.1, only adding
+new features. Please note, though, two possible issues:
+
+* If you did not explicitly depend on `accounts-base` before, you must add this
+  dependency, since it is no longer implied by `accounts-sansdtorm`.
+* The `/.sandstorm-credentials` endpoint no longer exists. If you were directly
+  fetching this undocumented endpoint before, you will need to switch your code
+  to use `Meteor.sandstormUser()`.

+ 186 - 0
packages/wekan-accounts-sandstorm/client.js

@@ -0,0 +1,186 @@
+// Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors
+// Licensed under the MIT License:
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+function loginWithSandstorm(connection, apiHost, apiToken) {
+  // Log in the connection using Sandstorm authentication.
+  //
+  // After calling this, connection.sandstormUser() will reactively return an object containing
+  // Sansdstorm user info, including permissions as authenticated by the server. Even if the user
+  // is anonymous, this information is returned. `sandstormUser()` returns `null` up until the
+  // point where login succeeds.
+
+  // How this works:
+  // 1. We create a cryptographically random token, which we're going to send to the server twice.
+  // 2. We make a method call to log in with this token. The server initially has no idea who
+  //    is calling and will block waiting for info. (The method is marked "wait" on the client side
+  //    so that further method calls are blocked until login completes.)
+  // 3. We also send an XHR with the same token. When the server receives the XHR, it harvests the
+  //    Sandstorm headers, looks for the corresponding login method call, marks its connection as
+  //    logged in, and then lets it return.
+  //
+  // We don't actually use Accounts.callLoginMethod() because we don't need or want the
+  // "resume token" logic. On a disconnect, we need to re-authenticate, because the user's
+  // permissions may have changed (indeed, this may be the reason for the disconnect).
+
+  // If the connection doesn't already have a sandstormUser() method, add it now.
+  if (!connection._sandstormUser) {
+    connection._sandstormUser = new ReactiveVar(null);
+    connection.sandstormUser = connection._sandstormUser.get.bind(connection._sandstormUser);
+  }
+
+  // Generate a random token which we'll send both over an XHR and over DDP at the same time.
+  var token = Random.secret();
+
+  var waiting = true;          // We'll keep retrying XHRs until the method returns.
+  var reconnected = false;
+
+  var onResultReceived = function (error, result) {
+    waiting = false;
+
+    if (error) {
+      // ignore for now; loggedInAndDataReadyCallback() will get the error too
+    } else {
+      connection.onReconnect = function () {
+        reconnected = true;
+        loginWithSandstorm(connection, apiHost, apiToken);
+      };
+    }
+  };
+
+  var loggedInAndDataReadyCallback = function (error, result) {
+    if (reconnected) {
+      // Oh, we're already on a future connection attempt. Don't mess with anything.
+      return;
+    }
+
+    if (error) {
+      console.error("loginWithSandstorm failed:", error);
+    } else {
+      connection._sandstormUser.set(result.sandstorm);
+      connection.setUserId(result.userId);
+    }
+  };
+
+  Meteor.apply("loginWithSandstorm", [token],
+      {wait: true, onResultReceived: onResultReceived},
+      loggedInAndDataReadyCallback);
+
+  var sendXhr = function () {
+    if (!waiting) return;  // Method call finished.
+
+    headers = {"Content-Type": "application/x-sandstorm-login-token"};
+
+    var testInfo = localStorage.sandstormTestUserInfo;
+    if (testInfo) {
+      testInfo = JSON.parse(testInfo);
+      if (testInfo.id) {
+        headers["X-Sandstorm-User-Id"] = testInfo.id;
+      }
+      if (testInfo.name) {
+        headers["X-Sandstorm-Username"] = encodeURI(testInfo.name);
+      }
+      if (testInfo.picture) {
+        headers["X-Sandstorm-User-Picture"] = testInfo.picture;
+      }
+      if (testInfo.permissions) {
+        headers["X-Sandstorm-Permissions"] = testInfo.permissions.join(",");
+      }
+      if (testInfo.preferredHandle) {
+        headers["X-Sandstorm-Preferred-Handle"] = testInfo.preferredHandle;
+      }
+      if (testInfo.pronouns) {
+        headers["X-Sandstorm-User-Pronouns"] = testInfo.pronouns;
+      }
+    }
+
+    var postUrl = "/.sandstorm-login";
+    // Sandstorm mobile apps need to point at a different host and use an Authorization token.
+    if (apiHost) {
+      postUrl = apiHost + postUrl;
+      headers.Authorization = "Bearer " + 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);
+
+        if (waiting) {
+          // Try again in a second.
+          Meteor.setTimeout(sendXhr, 1000);
+        }
+      }
+    });
+  };
+
+  // Wait until the connection is up before we start trying to send XHRs.
+  var stopImmediately = false;  // Unfortunately, Tracker.autorun() runs the first time inline.
+  var handle = Tracker.autorun(function () {
+    if (!waiting) {
+      if (handle) {
+        handle.stop();
+      } else {
+        stopImmediately = true;
+      }
+      return;
+    } else if (connection.status().connected) {
+      if (handle) {
+        handle.stop();
+      } else {
+        stopImmediately = true;
+      }
+
+      // Wait 10ms before our first attempt to send the rendezvous XHR because if it arrives
+      // before the method call it will be rejected.
+      Meteor.setTimeout(sendXhr, 10);
+    }
+  });
+  if (stopImmediately) handle.stop();
+}
+
+if (__meteor_runtime_config__.SANDSTORM) {
+  // Auto-login the main Meteor connection.
+  loginWithSandstorm(Meteor.connection, __meteor_runtime_config__.SANDSTORM_API_HOST,
+    __meteor_runtime_config__.SANDSTORM_API_TOKEN);
+
+  if (Package["accounts-base"]) {
+    // Make Meteor.loggingIn() work by calling a private method of accounts-base. If this breaks then
+    // maybe we should just overwrite Meteor.loggingIn() instead.
+    Tracker.autorun(function () {
+      Package["accounts-base"].Accounts._setLoggingIn(!Meteor.connection.sandstormUser());
+    });
+  }
+
+  Meteor.sandstormUser = function () {
+    return Meteor.connection.sandstormUser();
+  };
+
+  SandstormAccounts = {
+    setTestUserInfo: function (info) {
+      localStorage.sandstormTestUserInfo = JSON.stringify(info);
+      loginWithSandstorm(Meteor.connection, __meteor_runtime_config__.SANDSTORM_API_HOST,
+         __meteor_runtime_config__.SANDSTORM_API_TOKEN);
+    }
+  };
+}

+ 45 - 0
packages/wekan-accounts-sandstorm/package.js

@@ -0,0 +1,45 @@
+// Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors
+// Licensed under the MIT License:
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+Package.describe({
+  summary: "Login service for Sandstorm.io applications",
+  version: "0.7.0",
+  name: "wekan-accounts-sandstorm",
+  git: "https://github.com/sandstorm-io/meteor-accounts-sandstorm.git"
+});
+
+Package.onUse(function(api) {
+  api.versionsFrom('1.5.1');
+
+  api.use('random', ['client', 'server']);
+  api.use('accounts-base@2.2.2', ['client', 'server'], {weak: true});
+  api.use('webapp', 'server');
+  api.use('http', 'client');
+  api.use('tracker', 'client');
+  api.use('reactive-var', 'client');
+  api.use('check', 'server');
+  api.use('ddp-server', 'server');
+
+  api.addFiles("client.js", "client");
+  api.addFiles("server.js", "server");
+
+  api.export("SandstormAccounts", "client");
+});

+ 210 - 0
packages/wekan-accounts-sandstorm/server.js

@@ -0,0 +1,210 @@
+// Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors
+// Licensed under the MIT License:
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+if (process.env.SANDSTORM) {
+  __meteor_runtime_config__.SANDSTORM = true;
+}
+
+if (__meteor_runtime_config__.SANDSTORM) {
+  if (Package["accounts-base"]) {
+    // Highlander Mode: Disable all non-Sandstorm login mechanisms.
+    Package["accounts-base"].Accounts.validateLoginAttempt(function (attempt) {
+      if (!attempt.allowed) {
+        return false;
+      }
+      if (attempt.type !== "sandstorm") {
+        throw new Meteor.Error(403, "Non-Sandstorm login mechanisms disabled on Sandstorm.");
+      }
+      return true;
+    });
+    Package["accounts-base"].Accounts.validateNewUser(function (user) {
+      if (!user.services.sandstorm) {
+        throw new Meteor.Error(403, "Non-Sandstorm login mechanisms disabled on Sandstorm.");
+      }
+      return true;
+    });
+  }
+
+  var Future = Npm.require("fibers/future");
+
+  var inMeteor = Meteor.bindEnvironment(function (callback) {
+    callback();
+  });
+
+  var logins = {};
+  // Maps tokens to currently-waiting login method calls.
+
+  if (Package["accounts-base"]) {
+    Meteor.users.createIndex("services.sandstorm.id", {unique: 1, sparse: 1});
+  }
+
+  Meteor.onConnection(function (connection) {
+    connection._sandstormUser = null;
+    connection._sandstormSessionId = null;
+    connection._sandstormTabId = null;
+    connection.sandstormUser = function () {
+      if (!connection._sandstormUser) {
+        throw new Meteor.Error(400, "Client did not complete authentication handshake.");
+      }
+      return this._sandstormUser;
+    };
+    connection.sandstormSessionId = function () {
+      if (!connection._sandstormUser) {
+        throw new Meteor.Error(400, "Client did not complete authentication handshake.");
+      }
+      return this._sandstormSessionId;
+    }
+    connection.sandstormTabId = function () {
+      if (!connection._sandstormUser) {
+        throw new Meteor.Error(400, "Client did not complete authentication handshake.");
+      }
+      return this._sandstormTabId;
+    }
+  });
+
+  Meteor.methods({
+    loginWithSandstorm: function (token) {
+      check(token, String);
+
+      var future = new Future();
+
+      logins[token] = future;
+
+      var timeout = setTimeout(function () {
+        future.throw(new Meteor.Error("timeout", "Gave up waiting for login rendezvous XHR."));
+      }, 10000);
+
+      var info;
+      try {
+        info = future.wait();
+      } finally {
+        clearTimeout(timeout);
+        delete logins[token];
+      }
+
+      // Set connection info. The call to setUserId() resets all publishes. We update the
+      // connection's sandstorm info first so that when the publishes are re-run they'll see the
+      // new info. In theory we really want to update it exactly when this.userId is updated, but
+      // we'd have to dig into Meteor internals to pull that off. Probably updating it a little
+      // early is fine?
+      //
+      // Note that calling setUserId() with the same ID a second time still goes through the motions
+      // of restarting all subscriptions, which is important if the permissions changed. Hopefully
+      // Meteor won't decide to "optimize" this by returning early if the user ID hasn't changed.
+      this.connection._sandstormUser = info.sandstorm;
+      this.connection._sandstormSessionId = info.sessionId;
+      this.connection._sandstormTabId = info.tabId;
+      this.setUserId(info.userId);
+
+      return info;
+    }
+  });
+
+  WebApp.rawConnectHandlers.use(function (req, res, next) {
+    if (req.url === "/.sandstorm-login") {
+      handlePostToken(req, res);
+      return;
+    }
+    return next();
+  });
+
+  function readAll(stream) {
+    var future = new Future();
+
+    var chunks = [];
+    stream.on("data", function (chunk) {
+      chunks.push(chunk.toString());
+    });
+    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);
+      }
+    });
+  });
+}