Added Sandstorm Accounts.

This commit is contained in:
Lauri Ojansivu 2022-05-02 23:30:01 +03:00
parent 40aae378b6
commit f260b7ba88
20 changed files with 889 additions and 2 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
local

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"),
]
);

View file

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

View file

@ -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()`.

View file

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

View file

@ -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.8.2');
api.use('random', ['client', 'server']);
api.use('accounts-base@~2.2.3-rc272.0', ['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");
});

View file

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