merge master changes

This commit is contained in:
viehlieb 2022-07-08 11:55:32 +02:00
commit 5df5c7f5d7
401 changed files with 142692 additions and 15007 deletions

View file

@ -93,9 +93,13 @@ Triggers.runTriggers = function(triggers, context, redirectFn, after) {
throw new Error("already redirected");
}
/*
// Commenting out, so that redirects work when not in sync.
// https://github.com/wekan/wekan/issues/4514
if(!inCurrentLoop) {
throw new Error("redirect needs to be done in sync");
}
*/
if(!url) {
throw new Error("trigger redirect requires an URL");
@ -109,4 +113,4 @@ Triggers.runTriggers = function(triggers, context, redirectFn, after) {
function doStop() {
abort = true;
}
};
};

View file

@ -5,18 +5,15 @@ Package.describe({
git: 'https://github.com/wekan/markdown.git',
});
// Before Meteor 0.9?
if(!Package.onUse) Package.onUse = Package.on_use;
Package.onUse(function (api) {
if(api.versionsFrom) api.versionsFrom('1.8.2');
api.use('templating');
api.use("ecmascript", ['server', 'client']);
api.use("ecmascript", ['server', 'client']);
api.export('Markdown', ['server', 'client']);
api.use('ui', 'client', {weak: true});
api.use('ui', 'client', {weak: true});
api.add_files('src/template-integration.js', 'client');
api.addFiles('src/template-integration.js', 'client');
});

View file

@ -1,88 +0,0 @@
This is a merged repository of useful forks of: atoy40:accounts-cas
===================
([(https://atmospherejs.com/atoy40/accounts-cas](https://atmospherejs.com/atoy40/accounts-cas))
## Essential improvements by ppoulard to atoy40 and xaionaro versions
* Added support of CAS attributes
With this plugin, you can pick CAS attributes : https://github.com/joshchan/node-cas/wiki/CAS-Attributes
Moved to Wekan GitHub org from from https://github.com/ppoulard/meteor-accounts-cas
## Install
```
cd ~site
mkdir packages
cd packages
git clone https://github.com/wekan/meteor-accounts-cas
cd ~site
meteor add wekan:accounts-cas
```
## Usage
Put CAS settings in Meteor.settings (for example using METEOR_SETTINGS env or --settings) like so:
If casVersion is not defined, it will assume you use CAS 1.0. (note by xaionaro: option `casVersion` seems to be just ignored in the code, ATM).
Server side settings:
```
Meteor.settings = {
"cas": {
"baseUrl": "https://cas.example.com/cas",
"autoClose": true,
"validateUrl":"https://cas.example.com/cas/p3/serviceValidate",
"casVersion": 3.0,
"attributes": {
"debug" : true
}
},
}
```
CAS `attributes` settings :
* `attributes`: by default `{}` : all default values below will apply
* * `debug` : by default `false` ; `true` will print to the server console the CAS attribute names to map, the CAS attributes values retrieved, if necessary the new user account created, and finally the user to use
* * `id` : by default, the CAS user is used for the user account, but you can specified another CAS attribute
* * `firstname` : by default `cas:givenName` ; but you can use your own CAS attribute
* * `lastname` : by default `cas:sn` (respectively) ; but you can use your own CAS attribute
* * `fullname` : by default unused, but if you specify your own CAS attribute, it will be used instead of the `firstname` + `lastname`
* * `mail` : by default `cas:mail`
Client side settings:
```
Meteor.settings = {
"public": {
"cas": {
"loginUrl": "https://cas.example.com/login",
"serviceParam": "service",
"popupWidth": 810,
"popupHeight": 610,
"popup": true,
}
}
}
```
`proxyUrl` is not required. Setup [ROOT_URL](http://docs.meteor.com/api/core.html#Meteor-absoluteUrl) environment variable instead.
Then, to start authentication, you have to call the following method from the client (for example in a click handler) :
```
Meteor.loginWithCas([callback]);
```
It must open a popup containing you CAS login form or redirect to the CAS login form (depending on "popup" setting).
If popup is disabled (== false), then it's required to execute `Meteor.initCas([callback])` in `Meteor.startup` of the client side. ATM, `Meteor.initCas()` completes authentication.
## Examples
* [https://devel.mephi.ru/dyokunev/start-mephi-ru](https://devel.mephi.ru/dyokunev/start-mephi-ru)

View file

@ -1,117 +0,0 @@
function addParameterToURL(url, param){
var urlSplit = url.split('?');
return url+(urlSplit.length>0 ? '?':'&') + param;
}
Meteor.initCas = function(callback) {
const casTokenMatch = window.location.href.match(/[?&]casToken=([^&]+)/);
if (casTokenMatch == null) {
return;
}
window.history.pushState('', document.title, window.location.href.replace(/([&?])casToken=[^&]+[&]?/, '$1').replace(/[?&]+$/g, ''));
Accounts.callLoginMethod({
methodArguments: [{ cas: { credentialToken: casTokenMatch[1] } }],
userCallback: function(err){
if (err == null) {
// should we do anything on success?
}
if (callback != null) {
callback(err);
}
}
});
}
Meteor.loginWithCas = function(options, callback) {
var credentialToken = Random.id();
if (!Meteor.settings.public &&
!Meteor.settings.public.cas &&
!Meteor.settings.public.cas.loginUrl) {
return;
}
var settings = Meteor.settings.public.cas;
var backURL = window.location.href.replace('#', '');
if (options != null && options.redirectUrl != null)
backURL = options.redirectUrl;
var serviceURL = addParameterToURL(backURL, 'casToken='+credentialToken);
var loginUrl = settings.loginUrl +
"?" + (settings.serviceParam || "service") + "=" +
encodeURIComponent(serviceURL)
if (settings.popup == false) {
window.location = loginUrl;
return;
}
var popup = openCenteredPopup(
loginUrl,
settings.width || 800,
settings.height || 600
);
var checkPopupOpen = setInterval(function() {
try {
if(popup && popup.document && popup.document.getElementById('popupCanBeClosed')) {
popup.close();
}
// Fix for #328 - added a second test criteria (popup.closed === undefined)
// to humour this Android quirk:
// http://code.google.com/p/android/issues/detail?id=21061
var popupClosed = popup.closed || popup.closed === undefined;
} catch (e) {
// For some unknown reason, IE9 (and others?) sometimes (when
// the popup closes too quickly?) throws "SCRIPT16386: No such
// interface supported" when trying to read 'popup.closed'. Try
// again in 100ms.
return;
}
if (popupClosed) {
clearInterval(checkPopupOpen);
// check auth on server.
Accounts.callLoginMethod({
methodArguments: [{ cas: { credentialToken: credentialToken } }],
userCallback: err => {
// Fix redirect bug after login successfully
if (!err) {
window.location.href = '/';
}
}
});
}
}, 100);
};
var openCenteredPopup = function(url, width, height) {
var screenX = typeof window.screenX !== 'undefined'
? window.screenX : window.screenLeft;
var screenY = typeof window.screenY !== 'undefined'
? window.screenY : window.screenTop;
var outerWidth = typeof window.outerWidth !== 'undefined'
? window.outerWidth : document.body.clientWidth;
var outerHeight = typeof window.outerHeight !== 'undefined'
? window.outerHeight : (document.body.clientHeight - 22);
// XXX what is the 22?
// Use `outerWidth - width` and `outerHeight - height` for help in
// positioning the popup centered relative to the current window
var left = screenX + (outerWidth - width) / 2;
var top = screenY + (outerHeight - height) / 2;
var features = ('width=' + width + ',height=' + height +
',left=' + left + ',top=' + top + ',scrollbars=yes');
var newwindow = window.open(url, '_blank', features);
if (newwindow.focus)
newwindow.focus();
return newwindow;
};

View file

@ -1,71 +0,0 @@
Meteor.loginWithCas = function(callback) {
var credentialToken = Random.id();
if (!Meteor.settings.public &&
!Meteor.settings.public.cas &&
!Meteor.settings.public.cas.loginUrl) {
return;
}
var settings = Meteor.settings.public.cas;
var loginUrl = settings.loginUrl +
"?" + (settings.service || "service") + "=" +
Meteor.absoluteUrl('_cas/') +
credentialToken;
var fail = function (err) {
Meteor._debug("Error from OAuth popup: " + JSON.stringify(err));
};
// When running on an android device, we sometimes see the
// `pageLoaded` callback fire twice for the final page in the OAuth
// popup, even though the page only loads once. This is maybe an
// Android bug or maybe something intentional about how onPageFinished
// works that we don't understand and isn't well-documented.
var oauthFinished = false;
var pageLoaded = function (event) {
if (oauthFinished) {
return;
}
if (event.url.indexOf(Meteor.absoluteUrl('_cas')) === 0) {
oauthFinished = true;
// On iOS, this seems to prevent "Warning: Attempt to dismiss from
// view controller <MainViewController: ...> while a presentation
// or dismiss is in progress". My guess is that the last
// navigation of the OAuth popup is still in progress while we try
// to close the popup. See
// https://issues.apache.org/jira/browse/CB-2285.
//
// XXX Can we make this timeout smaller?
setTimeout(function () {
popup.close();
// check auth on server.
Accounts.callLoginMethod({
methodArguments: [{ cas: { credentialToken: credentialToken } }],
userCallback: callback
});
}, 100);
}
};
var onExit = function () {
popup.removeEventListener('loadstop', pageLoaded);
popup.removeEventListener('loaderror', fail);
popup.removeEventListener('exit', onExit);
};
var popup = window.open(loginUrl, '_blank', 'location=no,hidden=no');
popup.addEventListener('loadstop', pageLoaded);
popup.addEventListener('loaderror', fail);
popup.addEventListener('exit', onExit);
popup.show();
};

View file

@ -1,304 +0,0 @@
"use strict";
const Fiber = Npm.require('fibers');
const https = Npm.require('https');
const url = Npm.require('url');
const xmlParser = Npm.require('xml2js');
// Library
class CAS {
constructor(options) {
options = options || {};
if (!options.validate_url) {
throw new Error('Required CAS option `validateUrl` missing.');
}
if (!options.service) {
throw new Error('Required CAS option `service` missing.');
}
const cas_url = url.parse(options.validate_url);
if (cas_url.protocol != 'https:' ) {
throw new Error('Only https CAS servers are supported.');
} else if (!cas_url.hostname) {
throw new Error('Option `validateUrl` must be a valid url like: https://example.com/cas/serviceValidate');
} else {
this.hostname = cas_url.host;
this.port = 443;// Should be 443 for https
this.validate_path = cas_url.pathname;
}
this.service = options.service;
}
validate(ticket, callback) {
const httparams = {
host: this.hostname,
port: this.port,
path: url.format({
pathname: this.validate_path,
query: {ticket: ticket, service: this.service},
}),
};
https.get(httparams, (res) => {
res.on('error', (e) => {
console.log('error' + e);
callback(e);
});
// Read result
res.setEncoding('utf8');
let response = '';
res.on('data', (chunk) => {
response += chunk;
});
res.on('end', (error) => {
if (error) {
console.log('error callback');
console.log(error);
callback(undefined, false);
} else {
xmlParser.parseString(response, (err, result) => {
if (err) {
console.log('Bad response format.');
callback({message: 'Bad response format. XML could not parse it'});
} else {
if (result['cas:serviceResponse'] == null) {
console.log('Empty response.');
callback({message: 'Empty response.'});
}
if (result['cas:serviceResponse']['cas:authenticationSuccess']) {
const userData = {
id: result['cas:serviceResponse']['cas:authenticationSuccess'][0]['cas:user'][0].toLowerCase(),
};
const attributes = result['cas:serviceResponse']['cas:authenticationSuccess'][0]['cas:attributes'][0];
// Check allowed ldap groups if exist (array only)
// example cas settings : "allowedLdapGroups" : ["wekan", "admin"],
let findedGroup = false;
const allowedLdapGroups = Meteor.settings.cas.allowedLdapGroups || false;
for (const fieldName in attributes) {
if (allowedLdapGroups && fieldName === 'cas:memberOf') {
for (const groups in attributes[fieldName]) {
const str = attributes[fieldName][groups];
if (!Array.isArray(allowedLdapGroups)) {
callback({message: 'Settings "allowedLdapGroups" must be an array'});
}
for (const allowedLdapGroup in allowedLdapGroups) {
if (str.search(`cn=${allowedLdapGroups[allowedLdapGroup]}`) >= 0) {
findedGroup = true;
}
}
}
}
userData[fieldName] = attributes[fieldName][0];
}
if (allowedLdapGroups && !findedGroup) {
callback({message: 'Group not finded.'}, false);
} else {
callback(undefined, true, userData);
}
} else {
callback(undefined, false);
}
}
});
}
});
});
}
}
////// END OF CAS MODULE
let _casCredentialTokens = {};
let _userData = {};
//RoutePolicy.declare('/_cas/', 'network');
// Listen to incoming OAuth http requests
WebApp.connectHandlers.use((req, res, next) => {
// Need to create a Fiber since we're using synchronous http calls and nothing
// else is wrapping this in a fiber automatically
Fiber(() => {
middleware(req, res, next);
}).run();
});
const middleware = (req, res, next) => {
// Make sure to catch any exceptions because otherwise we'd crash
// the runner
try {
urlParsed = url.parse(req.url, true);
// Getting the ticket (if it's defined in GET-params)
// If no ticket, then request will continue down the default
// middlewares.
const query = urlParsed.query;
if (query == null) {
next();
return;
}
const ticket = query.ticket;
if (ticket == null) {
next();
return;
}
const serviceUrl = Meteor.absoluteUrl(urlParsed.href.replace(/^\//g, '')).replace(/([&?])ticket=[^&]+[&]?/g, '$1').replace(/[?&]+$/g, '');
const redirectUrl = serviceUrl;//.replace(/([&?])casToken=[^&]+[&]?/g, '$1').replace(/[?&]+$/g, '');
// get auth token
const credentialToken = query.casToken;
if (!credentialToken) {
end(res, redirectUrl);
return;
}
// validate ticket
casValidate(req, ticket, credentialToken, serviceUrl, () => {
end(res, redirectUrl);
});
} catch (err) {
console.log("account-cas: unexpected error : " + err.message);
end(res, redirectUrl);
}
};
const casValidate = (req, ticket, token, service, callback) => {
// get configuration
if (!Meteor.settings.cas/* || !Meteor.settings.cas.validate*/) {
throw new Error('accounts-cas: unable to get configuration.');
}
const cas = new CAS({
validate_url: Meteor.settings.cas.validateUrl,
service: service,
version: Meteor.settings.cas.casVersion
});
cas.validate(ticket, (err, status, userData) => {
if (err) {
console.log("accounts-cas: error when trying to validate " + err);
console.log(err);
} else {
if (status) {
console.log(`accounts-cas: user validated ${userData.id}
(${JSON.stringify(userData)})`);
_casCredentialTokens[token] = { id: userData.id };
_userData = userData;
} else {
console.log("accounts-cas: unable to validate " + ticket);
}
}
callback();
});
return;
};
/*
* Register a server-side login handle.
* It is call after Accounts.callLoginMethod() is call from client.
*/
Accounts.registerLoginHandler((options) => {
if (!options.cas)
return undefined;
if (!_hasCredential(options.cas.credentialToken)) {
throw new Meteor.Error(Accounts.LoginCancelledError.numericError,
'no matching login attempt found');
}
const result = _retrieveCredential(options.cas.credentialToken);
const attrs = Meteor.settings.cas.attributes || {};
// CAS keys
const fn = attrs.firstname || 'cas:givenName';
const ln = attrs.lastname || 'cas:sn';
const full = attrs.fullname;
const mail = attrs.mail || 'cas:mail'; // or 'email'
const uid = attrs.id || 'id';
if (attrs.debug) {
if (full) {
console.log(`CAS fields : id:"${uid}", fullname:"${full}", mail:"${mail}"`);
} else {
console.log(`CAS fields : id:"${uid}", firstname:"${fn}", lastname:"${ln}", mail:"${mail}"`);
}
}
const name = full ? _userData[full] : _userData[fn] + ' ' + _userData[ln];
// https://docs.meteor.com/api/accounts.html#Meteor-users
options = {
// _id: Meteor.userId()
username: _userData[uid], // Unique name
emails: [
{ address: _userData[mail], verified: true }
],
createdAt: new Date(),
profile: {
// The profile is writable by the user by default.
name: name,
fullname : name,
email : _userData[mail]
},
active: true,
globalRoles: ['user']
};
if (attrs.debug) {
console.log(`CAS response : ${JSON.stringify(result)}`);
}
let user = Meteor.users.findOne({ 'username': options.username });
if (! user) {
if (attrs.debug) {
console.log(`Creating user account ${JSON.stringify(options)}`);
}
const userId = Accounts.insertUserDoc({}, options);
user = Meteor.users.findOne(userId);
}
if (attrs.debug) {
console.log(`Using user account ${JSON.stringify(user)}`);
}
return { userId: user._id };
});
const _hasCredential = (credentialToken) => {
return _.has(_casCredentialTokens, credentialToken);
}
/*
* Retrieve token and delete it to avoid replaying it.
*/
const _retrieveCredential = (credentialToken) => {
const result = _casCredentialTokens[credentialToken];
delete _casCredentialTokens[credentialToken];
return result;
}
const closePopup = (res) => {
if (Meteor.settings.cas && Meteor.settings.cas.popup == false) {
return;
}
res.writeHead(200, {'Content-Type': 'text/html'});
const content = '<html><body><div id="popupCanBeClosed"></div></body></html>';
res.end(content, 'utf-8');
}
const redirect = (res, whereTo) => {
res.writeHead(302, {'Location': whereTo});
const content = '<html><head><meta http-equiv="refresh" content="0; url='+whereTo+'" /></head><body>Redirection to <a href='+whereTo+'>'+whereTo+'</a></body></html>';
res.end(content, 'utf-8');
return
}
const end = (res, whereTo) => {
if (Meteor.settings.cas && Meteor.settings.cas.popup == false) {
redirect(res, whereTo);
} else {
closePopup(res);
}
}

View file

@ -1,29 +0,0 @@
Package.describe({
summary: "CAS support for accounts",
version: "0.1.0",
name: "wekan:accounts-cas",
git: "https://github.com/wekan/meteor-accounts-cas"
});
Package.onUse(function(api) {
api.versionsFrom('METEOR@1.3.5.1');
api.use('routepolicy', 'server');
api.use('webapp', 'server');
api.use('accounts-base', ['client', 'server']);
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
api.use('underscore');
api.add_files('cas_client.js', 'web.browser');
api.add_files('cas_client_cordova.js', 'web.cordova');
api.add_files('cas_server.js', 'server');
});
Npm.depends({
xml2js: "0.4.17",
cas: "https://github.com/anrizal/node-cas/tarball/2baed530842e7a437f8f71b9346bcac8e84773cc"
});
Cordova.depends({
'cordova-plugin-inappbrowser': '1.2.0'
});

View file

@ -6,7 +6,7 @@ Package.describe({
documentation: 'README.md'
});
Package.on_use(function (api) {
Package.onUse(function (api) {
api.versionsFrom("METEOR@0.9.0");
api.add_files(['lib/autosize.js'], 'client');
api.addFiles(['lib/autosize.js'], 'client');
});

View file

@ -1,18 +0,0 @@
#.editorconfig
# Meteor adapted EditorConfig, http://EditorConfig.org
# By RaiX 2013
root = true
[*.js]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
charset = utf-8
max_line_length = 80
indent_brace_style = 1TBS
spaces_around_operators = true
quote_type = auto
# curly_bracket_next_line = true

View file

@ -1,2 +0,0 @@
.build*
versions.json

View file

@ -1,2 +0,0 @@
client/compatibility
packages

View file

@ -1,132 +0,0 @@
//.jshintrc
{
// JSHint Meteor Configuration File
// Match the Meteor Style Guide
//
// By @raix with contributions from @aldeed and @awatson1978
// Source https://github.com/raix/Meteor-jshintrc
//
// See http://jshint.com/docs/ for more details
"maxerr" : 50, // {int} Maximum error before stopping
// Enforcing
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
"camelcase" : true, // true: Identifiers must be in camelCase
"curly" : true, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
"indent" : 2, // {int} Number of spaces to use for indentation
"latedef" : false, // true: Require variables/functions to be defined before being used
"newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()`
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
"noempty" : true, // true: Prohibit use of empty blocks
"nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment)
"plusplus" : false, // true: Prohibit use of `++` & `--`
"quotmark" : false, // Quotation mark consistency:
// false : do nothing (default)
// true : ensure whatever is used is consistent
// "single" : require single quotes
// "double" : require double quotes
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
"unused" : true, // true: Require all defined variables be used
"strict" : true, // true: Requires all functions run in ES5 Strict Mode
"trailing" : true, // true: Prohibit trailing whitespaces
"maxparams" : false, // {int} Max number of formal params allowed per function
"maxdepth" : false, // {int} Max depth of nested blocks (within functions)
"maxstatements" : false, // {int} Max number statements per function
"maxcomplexity" : false, // {int} Max cyclomatic complexity per function
"maxlen" : 80, // {int} Max number of characters per line
// Relaxing
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
"boss" : false, // true: Tolerate assignments where comparisons would be expected
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
"esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
// (ex: `for each`, multiple try/catch, function expression…)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
"funcscope" : false, // true: Tolerate defining variables inside control statements"
"globalstrict" : true, // true: Allow global "use strict" (also enables 'strict')
"iterator" : false, // true: Tolerate using the `__iterator__` property
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
"laxcomma" : false, // true: Tolerate comma-first style coding
"loopfunc" : false, // true: Tolerate functions being defined in loops
"multistr" : false, // true: Tolerate multi-line strings
"proto" : false, // true: Tolerate using the `__proto__` property
"scripturl" : false, // true: Tolerate script-targeted URLs
"smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
"validthis" : false, // true: Tolerate using this in a non-constructor function
// Environments
"browser" : true, // Web Browser (window, document, etc)
"couch" : false, // CouchDB
"devel" : true, // Development/debugging (alert, confirm, etc)
"dojo" : false, // Dojo Toolkit
"jquery" : false, // jQuery
"mootools" : false, // MooTools
"node" : false, // Node.js
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
"prototypejs" : false, // Prototype and Scriptaculous
"rhino" : false, // Rhino
"worker" : false, // Web Workers
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface
//"meteor" : false, // Meteor.js
// Legacy
"nomen" : false, // true: Prohibit dangling `_` in variables
"onevar" : false, // true: Allow only one `var` statement per function
"passfail" : false, // true: Stop on first error
"white" : false, // true: Check against strict whitespace and indentation rules
// Custom globals, from http://docs.meteor.com, in the order they appear there
"globals" : {
"Meteor": false,
"DDP": false,
"Mongo": false, //Meteor.Collection renamed to Mongo.Collection
"Session": false,
"Accounts": false,
"Template": false,
"Blaze": false, //UI is being renamed Blaze
"UI": false,
"Match": false,
"check": false,
"Tracker": false, //Deps renamed to Tracker
"Deps": false,
"ReactiveVar": false,
"EJSON": false,
"HTTP": false,
"Email": false,
"Assets": false,
"Handlebars": false, // https://github.com/meteor/meteor/wiki/Handlebars
"Package": false,
// Meteor internals
"DDPServer": false,
"global": false,
"Log": false,
"MongoInternals": false,
"process": false,
"WebApp": false,
"WebAppInternals": false,
// globals useful when creating Meteor packages
"Npm": false,
"Tinytest": false,
// common Meteor packages
"Random": false,
"_": false, // Underscore.js
"$": false, // jQuery
"Router": false // iron-router
}
}

View file

@ -1,8 +0,0 @@
sudo: required
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/ejPSng | /bin/sh"
env:
- TEST_COMMAND=meteor

File diff suppressed because it is too large Load diff

View file

@ -1,353 +0,0 @@
## Master
## v1.14.2
* [flow-routing] fixed dependency on kadira:flow-router: now using the last non-Meteor@1.3 one
## v1.14.1
* fixed automatic update of weak dependencies on routing packages when publishing new versions
## v1.14.0
* [bulma] *new* `useraccounts:bulma` package to get UI templates styled for [Bulma](http://bulma.io/) (thanks to @dominikmayer)
* [flow-routing] better error management (merged https://github.com/meteor-useraccounts/flow-routing/pull/23 thanks @stubailo)
* [flow-routing] added support for FlowRouter 3 (merged https://github.com/meteor-useraccounts/flow-routing/pull/26 thanks @timothyarmes)
* [foundation-sites] *new* `useraccounts:foundation-sites` package to get UI templates styled for [Foundation for Sites 6](http://foundation.zurb.com/sites.html) (thanks to @venetianthief)
* [materialize] Added row around recaptcha (thanks @qwIvan)
* some minor fixed to the Guide
## v1.13.1
* added language support to recaptcha (fixed https://github.com/meteor-useraccounts/core/issues/561 tnx @canesin)
* fixed validation trigger for select inputs (see discussion within https://github.com/meteor-useraccounts/core/issues/569 tnx @cunneen)
* change default value for `focusFirstInput` to get it disabled when running on Cordova (see https://github.com/meteor-useraccounts/core/issues/594 tnx @derwaldgeist)
* fixed regression about reCaptcha reset due to https://github.com/meteor-useraccounts/core/pull/565 (merged https://github.com/meteor-useraccounts/core/pull/597 tnx @jebh)
## v1.13.0
* [mdl] *new* `useraccounts:mdl` package to get UI templates styled for [Material Design Lite](http://www.getmdl.io/) (kudos to @kctang and @liquidautumn, thank you guys!).
* [flow-routing] added support for React-based layouts (merged https://github.com/meteor-useraccounts/flow-routing/pull/20 tnx @timothyarmes).
* [materialize] fixed offset problem for fullPageAtForm on medium screens.
* [materialize] fixed some margins (see https://github.com/meteor-useraccounts/materialize/issues/19).
* [iron-routing] fixed a problem with route paths (merged https://github.com/meteor-useraccounts/iron-routing/pull/8 tnx @trave7er).
* [core] updated dependency to softwarerero:accounts-t9n@1.1.7
* [core] fixed a bug with reCaptcha (merged https://github.com/meteor-useraccounts/core/pull/565 tnx @scsirdx).
* [core] added missing dependency on JQuery (merged https://github.com/meteor-useraccounts/core/pull/574 tnx @stubailo).
* [core] added postSignUpHook hook to let people modify newly created user objects (merged https://github.com/meteor-useraccounts/core/pull/586 tnx @shwaydogg)
* added [Meteor Icon](http://www.getmdl.io/) badges to all packages' README file.
## v1.12.4
* fixed input element classes for `useraccounts:materialize` (see https://github.com/meteor-useraccounts/materialize/pull/18)
* fixed query parameters look-up for `useraccounts:iron-routing`
* updated `useraccounts:polymer` to use Polimer 1.0 (see updated [boilerplate](https://github.com/meteor-useraccounts/boilerplates/tree/master/polymer) with some instructions for Meteor 1.2)
* updates and fixes for `useraccounts:flow-rounting` (see https://github.com/meteor-useraccounts/flow-routing/issues/12)
* improoved css for `useraccounts:semantic-ui`
* disallowed use of `signUp` state in case `forbidClientAccountCreation` is set (see #547)
* updated dependency on softwarerero:accounts-t9n to version 1.1.4
* a bit of linting here and there...
* a few typos correction and improvements to the [Guide](https://github.com/meteor-useraccounts/core/blob/master/Guide.md)
## v1.12.3
* fixed radio buttons for useraccounts:materialize (see https://github.com/meteor-useraccounts/core/issues/421)
* fixed query parameters pick up for useraccounts:iron-routing (see meteor-useraccounts/core#367)
* corrected few typos within the docs and removed unnecessary debug log
## v1.12.2
* various fixes and a bit of clean up for `useraccounts:flow-routing`
## v1.12.1
* fixed inifinite redirect loop for `ensuredSignedIn` within `useraccounts:flow-routing` (see https://github.com/meteor-useraccounts/flow-routing/issues/2)
## v1.12.0
* removed routing support from core: refer to [useraccounts:iron-routing](https://github.com/meteor-useraccounts/iron-routing) and [useraccounts:flow-routing](https://github.com/meteor-useraccounts/flow-routing) packages to get some ;-)
* added template level content protection (see new [Content Protection](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#content-protection) section)
* updated `useraccounts:semantic-ui` to SUI v2.0 (thanks @lumatijev)
* `displayName` configuration option for form fields now accepts also functions
* added the `focusFirstInput` configuration option
* fixed many typos and added/removed some sections in the Guide
## v1.11.1
* fixes for #410, #411, and #413
* Added a section about available internal states to the Guide (see [Internal States](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#internal-states)
## v1.11.0
* change `profile.username` to `profile.name` when using `lowercaseUsername` options (WARNING! this is a bit of a breaking change, see #388)
* removed possibly annoying warning (see #398)
* added a `preSignUpHook` to be possibly used to enrich the user profile just before new user registration (see #400)
* route configuration now accepts additional parameters to be passed to IR (see #409)
* some improvements to the docs
## v1.10.0
* more customizable texts (see 7d166b74f111e05b22ef2c7d93908441e242350d)
* added autofocus for the first input field of `atPwdForm`.
* fixed some texts configuration capability (see #380)
* various corrections/improvements to the docs
* allowed for `field.setError` to take in Boolean values (see #361)
* fixed bug with `Must be logged in` error message shown after sign out (see #321)
## v1.9.1
* aligned `useraccounts:unstyled` with the latest PRs
## v1.9.0
* resend verification email (see #349, thanks @dalgard)
* allow for a neutral message text to be displayed (see #314 and #317, thanks @dalgard)
* more configurable error texts (see [Errors Text](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#errors-text), plus #301 #342)
* fixed little redirect bug (see #315)
* added title configuration for `verifyEmail` state plus letting titles to be hidden by
setting the corresponding text to an empy string (see [Form Title](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#form-title))
## v1.8.1
* made (a fake) `ensureSignedIn` plugin available also on server side code (fixed #291)
## v1.8.0
* added `lowercaseUsername` configuration option (see [Configuration API Options](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#options))
* added `ensureSignedIn` plugin for Iron Router (see [Content Protection](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#content-protection))
* fixed `ensureSignedIn` regression (see #286)
## v1.7.1
* fixed routing regression (see #284)
* removed useless logs
## v1.7.0
* `useraccounts:materialize` to the suite! (Many thanks to @Kestanous!!!)
* fixed glitch within `ensureSignedIn` (see #278)
* added experimental support for [reChaptcha](https://www.google.com/recaptcha/intro/index.html) (see #268 and [reCaptcha Setup](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#recaptcha-setup), great work @theplatapi!)
* new `template` option for deeper input fields customization (see #273 and [Form Fields Configuration](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#form-fields-configuration))
* prevent access to `atChangePwd` for users not being logged in (see #207)
* use `Meteor.userID()` in place of `Meteor.user()` where possible to reduce reactive re-computations
* fixed bug with timed out redirects (see #263)
* fixed reactivity bug within `ensureSignedIn` (see #262)
* removed warning about MAIL_URL not being configured (see #267, #210)
* better `atNavButton` behaviour (see #265 tnx @adrianmc)
## v1.6.1
* updated deps for iron:router and softwarerero:accounts-t9n to latest versions
## v1.6.0
* moved the documentation to a separate file: [Guide](https://github.com/meteor-useraccounts/core/blob/master/Guide.md)
* fixed bug about calling `sibmitHook` (see #249 #252 tnx @dalgard)
* new `options` for field configuration (see #250 and [Extending Templates](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#extending-templates) tnx @dalgard)
* a bit of cleanup for docs (see #251 tnx @dalgard)
* capitalazed default value for display name and placeholder (see #247)
* switch to official `Accounts._hasPassword` (see [this](https://github.com/meteor/meteor/pull/2271) and [this](https://github.com/meteor/meteor/pull/3410), tnx @glasser)
* more sites using useraccounts: congrats to @nate-strauser and @msamoylov on their launches! (see [the list](https://github.com/meteor-useraccounts/core#whos-using-this))
* new landing page for the whole project and new live examples (still to be further improoved...) :) (see [useraccounts.meteor.com](https://useraccounts.meteor.com))
* added `transform` among the options for [field configuration](https://github.com/meteor-useraccounts/core/blob/master/Guide.md#form-fields-configuration)
* better behaviour for input value tranform/fix
* terms and agreements now showed also on enrollment form (see #253)
* link to singIn now shown also on forgot password form in case `forbidClientAccountCreation` is set to true (partial solution to #229)
* moved terms and agreements link right after the submit button (see #239)
## v1.5.0
* added `useraccounts:polymer` to the suite! (WIP, Thanks @kevohagan!!!)
* fixed a bug with atVerifyEmail route (see #241 and #173)
* little docs improovements
## v1.4.1
* updated dependency to softwarerero:accounts-t9n@1.0.5 to include Turkish language
* fixed `{{> atForm state='<state>'}}` which was no more working with Meteor@1.0.2 (see #217)
* fixed some text configuration (see #209, thanks @bumbleblym)
* fixed some typos into the docs (see #208, thanks @bumbleblym)
## v1.4.0
* added `useraccounts:ionic` to the suite! (Thanks @nickw!!!)
* updated `useraccounts:semantic-ui` to SemanticUI@1.0.0 (Thanks @lumatijev!!!)
* added `onLogoutHook` to be able to run code (custom redirects?) on `AccountsTemplates.logout` (see #191)
* added `onSubmitHook` among configuration parameters to be able to run code on form submission (might be useful for modals! see #201 and #180)
* submission button get now disabled also during fields (asynchronous) validation
* `enforceEmailVerification` now works also with username login (fixed #196)
* better IE compatibility (see #199)
* better input field validation flows to recover from previous errors (see #177)
* updated dependency to softwarerero:accounts-t9n@1.0.4
* new [Contributing section](https://github.com/meteor-useraccounts/core#contributing) among docs
* a few improvements and typo fixes for README.md
## v1.3.2 / 2014/11/25
* more robust logout pattern when dealing with routes protected with ensureSigndIn
## v1.3.1 / 2014/11/25
* updated dependency to iron:router@1.0.3
* fixed bug in linkClick (see #170)
* fixed bug in configureRoute
## v1.3.0 / 2014/11/23
* added support for [Ratchet](http://goratchet.com/): see [useraccounts:ratchet](https://atmospherejs.com/useraccounts/ratchet). Note: form validation is currently not supported by Ratchet!
* fixed bug in custom validation flow
* better default validation for `email` field (see #156)
* few corrections inside docs
* added `ensuredSignedIn` among configurable routes so that different `template` and `layoutTemplate` can be specified (fix for #160 and #98)
* added `socialLoginStyle` among the configuration options to select the login flow (`popup` or `redirect`) for 3rd party login services (see #163)
* fixed bug about fields ordering
## v1.2.3 / 2014/11/13
* put back in a `init` method dispalying a warning to preserve backward compatibility...
## v1.2.2 / 2014/11/12
* fixed bad redirect for cheange password route (see #154)
## v1.2.1 / 2014/11/12
* fixed regression due reactivity problems after fix for #139
## v1.2.0 / 2014/11/12
* **breaking change:** removed the need to call `Accounts.init()`
* added support for fields' validating state to display a 'loading' icon
* added support for fields' icon configuration
* added support for social buttons' icon configuration (see [this](https://github.com/meteor-useraccounts/core#social-button-icons) new section)
* added support for `meteor-developer` oauth service (see #147)
* fixed (special) fields ordering, see #144
* fixed ensureSignedIn (see #152)
* removed `new_password` and `new_password_again` special fields, simply use `password` and `password_again` from now on!
* better redirect behaviour when a logged in user lands on a sign-in/sign-up page: usual redirect is now performed. (see #139)
* better field validation patterns...
* updated dependency to irou:router@1.0.1
* updated dependency to softwarerero:accounts-t9n@1.0.2
* corrected many errors and typos inside the Documentation
## v1.1.1
## v1.1.0
* fixed `atNavButton` for useraccounts:unstyled
* fixed variour names and links in README files
## v1.1.0
* new template `atNavButton`
* added methos `AccountsTemplates.logout()` which redirects back to `homeRoutePath` when configured
* support for hidden fields
* url query parameters loaded into input fields -> useful mostly for hidden fields ;-)
* granted full control over field ordering (except for special fields...). see #135
* fixes for #130, #132
## v1.0.1
* fixed link to git repositories inside package.js files
## v1.0.0
* new names: no more splendido:accounts-templates:<somethig> but useraccounts:<somethig> !
* updated iron:router to v1.0.0
## v0.11.0
* added support for checkbox, select, and radio inputs
* added defaultState as referred in #125
* fixes for #127
## v0.10.0
* better texts configuration API (as for #117)
* prevPath fix
## v0.9.16
* updated iron:router to v0.9.4
## v0.9.15
* fixed #110
## v0.9.14
* fixed some redirection problems connected with `ensureSignedIn`
## v0.9.13
* experimental implementation for forbidding access with unverified email (see #108) through configuration flag `enforceEmailVerification`
* added options to hide links: hideSignInLink, hideSignUpLink
* fixed #107
## v0.9.12
* fixed #109
## v0.9.11
* better submit button disabling when no negative feedback is used
* fixed #105
## v0.9.10
* added `defaultLayout` to configuration options
* new callback parameter to `setState`
* better rendering behaviour on `ensureSignedIn`
## v0.9.9
* Fixed links for `reset-password`, `enroll-account`, and `verify-email`
## v0.9.8
* fixed checks for login services (see #93)
* minor updates to docs
## v0.9.7
* fixed #92, to permit the use of, e.g., `{{> atForm state="changePwd"}}` ( see [docs](https://github.com/splendido/accounts-templates-core#templates))
## v0.9.6
* fixed #91, pwdForm submission on signin page has no effect unless both password and usename/email are not empty
## v0.9.5
* show title on sign in also with other services
* moved sign in link below pwd form
* removed sign in link from forgot-pwd page (sign up link is still there!)
* added class at-btn to submit button
* added class at-signin to sign in link
* added class at-signup to sign up link
* added class at-pwd to forgot password link
* accounts-t9n dependency updated to @1.0.0
## v0.9.4
## Older versions (to be written)
* Fixes for #19, #24, #25, #26
* layoutTemplate option
* Better signup flow, with proper server side validation!
* Fixes for #15, and #16
* Do not show validation errors during sign in
* Do not show sign up link when account creation is disabled
* Better use of UnderscoreJS
* Corrected documentation for showAddRemoveServices
## v0.0.9
* added configuration parameter [`showAddRemoveServices`](https://github.com/splendido/accounts-templates-core#appearance)
* Fix ensureSignedIn for drawing correct template
## v0.0.8

View file

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 [@splendido](https://github.com/splendido)
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

@ -1,104 +0,0 @@
[![Meteor Icon](http://icon.meteor.com/package/useraccounts:core)](https://atmospherejs.com/useraccounts/core)
[![Build Status](https://travis-ci.org/meteor-useraccounts/core.svg?branch=master)](https://travis-ci.org/meteor-useraccounts/core)
# User Accounts
User Accounts is a suite of packages for the [Meteor.js](https://www.meteor.com/) platform. It provides highly customizable user accounts UI templates for many different front-end frameworks. At the moment it includes forms for sign in, sign up, forgot password, reset password, change password, enroll account, and link or remove of many 3rd party services.
## Some Details
The package `useraccounts:core` contains all the core logic and templates' helpers and events used by dependant packages providing styled versions of the accounts UI.
This means that developing a version of the UI with a different styling is just a matter of writing a few dozen of html lines, nothing more!
Thanks to [accounts-t9n](https://github.com/softwarerero/meteor-accounts-t9n) you can switch to your preferred language on the fly! Available languages are now: Arabic, Czech, French, German, Italian, Polish, Portuguese, Russian, Slovenian, Spanish, Swedish, Turkish and Vietnamese.
For basic routing and content protection, `useraccounts:core` integrates with either [flow-router](https://github.com/meteor-useraccounts/flow-routing) or [iron-router](https://atmospherejs.com/package/iron-router).
Any comments, suggestions, testing efforts, and PRs are very very welcome! Please use the [repository](https://github.com/meteor-useraccounts/ui) issues tracker for reporting bugs, problems, ideas, discussions, etc..
## The UserAccounts Guide
Detailed explanations of features and configuration options can be found in the <a href="https://github.com/meteor-useraccounts/core/blob/master/Guide.md" target="_blank">Guide</a>.
## Who's using this?
* [Abesea](https://abesea.com/)
* [backspace.academy](http://backspace.academy/)
* [bootstrappers.io](http://www.bootstrappers.io/)
* [crater.io](http://crater.io/)
* [Dechiper Chinese](http://app.decipherchinese.com/)
* [Henfood](http://labs.henesis.eu/henfood)
* [meteorgigs.io](https://www.meteorgigs.io/)
* [Orion](http://orionjs.org/)
* [Telescope](http://www.telesc.pe/)
* [We Work Meteor](http://www.weworkmeteor.com/)
Aren't you on the list?!
If you have a production app using accounts templates, let me know! I'd like to add your link to the above ones.
## Contributing
Contributors are very welcome. There are many things you can help with,
including finding and fixing bugs and creating examples for the brand new [wiki](https://github.com/meteor-useraccounts/wiki).
We're also working on `useraccounts@2.0` (see the [Milestone](https://github.com/meteor-useraccounts/core/milestones)) so you can also help
with an improved design or adding features.
Some guidelines below:
* **Questions**: Please create a new issue and label it as a `question`.
* **New Features**: If you'd like to work on a feature,
start by creating a 'Feature Design: Title' issue. This will let people bat it
around a bit before you send a full blown pull request. Also, you can create
an issue to discuss a design even if you won't be working on it.
* **Bugs**: If you think you found a bug, please create a "reproduction." This is a small project that demonstrates the problem as concisely as possible. If you think the bug can be reproduced with only a few steps a description by words might be enough though. The project should be cloneable from Github. Any bug reports without a reproduction that don't have an obvious solution will be marked as "awaiting-reproduction" and closed after a bit of time.
### Working Locally
This is useful if you're contributing code to useraccounts or just trying to modify something to suit your own specific needs.
##### Scenario A
1. Set up a local packages folder
2. Add the PACKAGE_DIRS environment variable to your .bashrc file
- Example: `export PACKAGE_DIRS="/full/path/topackages/folder"`
- Screencast: https://www.eventedmind.com/posts/meteor-versioning-and-packages
3. Clone the repository into your local packages directory
4. Add the package just like any other meteor core package like this: `meteor
add useraccounts:unstyled`
```bash
> cd /full/path/topackages/folder
> git clone https://github.com/meteor-useraccounts/semantic-ui.git
> cd your/project/path
> meteor add useraccounts:semantic-ui
> meteor
```
##### Scenario B
Like Scenario A, but skipping point 2.
Add the official package as usual with `meteor add useraccounts:semantic-ui` but then run your project like this:
```bash
> PACKAGE_DIRS="/full/path/topackages/folder" meteor
```
##### Scenario C
```bash
> cd your/project/path
> mkdir packages && cd packages
> git clone https://github.com/meteor-useraccounts/semantic-ui.git
> cd ..
> meteor add useraccounts:semantic-ui
> meteor
```
## Thanks
Anyone is welcome to contribute. Fork, make your changes, and then submit a pull request.
Thanks to [all those who have contributed code changes](https://github.com/meteor-useraccounts/ui/graphs/contributors) and all who have helped by submitting bug reports and feature ideas.
[![Support via Gittip](https://rawgithub.com/twolfson/gittip-badge/0.2.0/dist/gittip.png)](https://www.gittip.com/splendido/)

View file

@ -1,464 +0,0 @@
/* global
AT: false
*/
"use strict";
// Allowed Internal (client-side) States
AT.prototype.STATES = [
"changePwd", // Change Password
"enrollAccount", // Account Enrollment
"forgotPwd", // Forgot Password
"hide", // Nothing displayed
"resetPwd", // Reset Password
"signIn", // Sign In
"signUp", // Sign Up
"verifyEmail", // Email verification
"resendVerificationEmail", // Resend verification email
];
AT.prototype._loginType = "";
// Flag telling whether the whole form should appear disabled
AT.prototype._disabled = false;
// State validation
AT.prototype._isValidState = function(value) {
return _.contains(this.STATES, value);
};
// Flags used to avoid clearing errors and redirecting to previous route when
// signing in/up as a results of a call to ensureSignedIn
AT.prototype.avoidRedirect = false;
AT.prototype.avoidClearError = false;
// Token to be provided for routes like reset-password and enroll-account
AT.prototype.paramToken = null;
AT.prototype.loginType = function () {
return this._loginType;
};
AT.prototype.getparamToken = function() {
return this.paramToken;
};
// Getter for current state
AT.prototype.getState = function() {
return this.state.form.get("state");
};
// Getter for disabled state
AT.prototype.disabled = function() {
return this.state.form.equals("disabled", true) ? "disabled" : undefined;
};
// Setter for disabled state
AT.prototype.setDisabled = function(value) {
check(value, Boolean);
return this.state.form.set("disabled", value);
};
// Setter for current state
AT.prototype.setState = function(state, callback) {
check(state, String);
if (!this._isValidState(state) || (this.options.forbidClientAccountCreation && state === 'signUp')) {
throw new Meteor.Error(500, "Internal server error", "accounts-templates-core package got an invalid state value!");
}
this.state.form.set("state", state);
if (!this.avoidClearError) {
this.clearState();
}
this.avoidClearError = false;
if (_.isFunction(callback)) {
callback();
}
};
AT.prototype.clearState = function() {
_.each(this._fields, function(field) {
field.clearStatus();
});
var form = this.state.form;
form.set("error", null);
form.set("result", null);
form.set("message", null);
AccountsTemplates.setDisabled(false);
};
AT.prototype.clearError = function() {
this.state.form.set("error", null);
};
AT.prototype.clearResult = function() {
this.state.form.set("result", null);
};
AT.prototype.clearMessage = function() {
this.state.form.set("message", null);
};
// Initialization
AT.prototype.init = function() {
console.warn("[AccountsTemplates] There is no more need to call AccountsTemplates.init()! Simply remove the call ;-)");
};
AT.prototype._init = function() {
if (this._initialized) {
return;
}
var usernamePresent = this.hasField("username");
var emailPresent = this.hasField("email");
if (usernamePresent && emailPresent) {
this._loginType = "username_and_email";
} else {
this._loginType = usernamePresent ? "username" : "email";
}
if (this._loginType === "username_and_email") {
// Possibly adds the field username_and_email in case
// it was not configured
if (!this.hasField("username_and_email")) {
this.addField({
_id: "username_and_email",
type: "text",
displayName: "usernameOrEmail",
placeholder: "usernameOrEmail",
required: true,
});
}
}
// Only in case password confirmation is required
if (this.options.confirmPassword) {
// Possibly adds the field password_again in case
// it was not configured
if (!this.hasField("password_again")) {
var pwdAgain = _.clone(this.getField("password"));
pwdAgain._id = "password_again";
pwdAgain.displayName = {
"default": "passwordAgain",
changePwd: "newPasswordAgain",
resetPwd: "newPasswordAgain",
};
pwdAgain.placeholder = {
"default": "passwordAgain",
changePwd: "newPasswordAgain",
resetPwd: "newPasswordAgain",
};
this.addField(pwdAgain);
}
} else {
if (this.hasField("password_again")) {
throw new Error("AccountsTemplates: a field password_again was added but confirmPassword is set to false!");
}
}
// Possibly adds the field current_password in case
// it was not configured
if (this.options.enablePasswordChange) {
if (!this.hasField("current_password")) {
this.addField({
_id: "current_password",
type: "password",
displayName: "currentPassword",
placeholder: "currentPassword",
required: true,
});
}
}
// Ensuser the right order of special fields
var moveFieldAfter = function(fieldName, referenceFieldName) {
var fieldIds = AccountsTemplates.getFieldIds();
var refFieldId = _.indexOf(fieldIds, referenceFieldName);
// In case the reference field is not present, just return...
if (refFieldId === -1) {
return;
}
var fieldId = _.indexOf(fieldIds, fieldName);
// In case the sought field is not present, just return...
if (fieldId === -1) {
return;
}
if (fieldId !== -1 && fieldId !== (refFieldId + 1)) {
// removes the field
var field = AccountsTemplates._fields.splice(fieldId, 1)[0];
// push the field right after the reference field position
var newFieldIds = AccountsTemplates.getFieldIds();
var newReferenceFieldId = _.indexOf(newFieldIds, referenceFieldName);
AccountsTemplates._fields.splice(newReferenceFieldId + 1, 0, field);
}
};
// Ensuser the right order of special fields
var moveFieldBefore = function(fieldName, referenceFieldName) {
var fieldIds = AccountsTemplates.getFieldIds();
var refFieldId = _.indexOf(fieldIds, referenceFieldName);
// In case the reference field is not present, just return...
if (refFieldId === -1) {
return;
}
var fieldId = _.indexOf(fieldIds, fieldName);
// In case the sought field is not present, just return...
if (fieldId === -1) {
return;
}
if (fieldId !== -1 && fieldId !== (refFieldId - 1)) {
// removes the field
var field = AccountsTemplates._fields.splice(fieldId, 1)[0];
// push the field right after the reference field position
var newFieldIds = AccountsTemplates.getFieldIds();
var newReferenceFieldId = _.indexOf(newFieldIds, referenceFieldName);
AccountsTemplates._fields.splice(newReferenceFieldId, 0, field);
}
};
// The final order should be something like:
// - username
// - email
// - username_and_email
// - password
// - password_again
//
// ...so lets do it in reverse order...
moveFieldAfter("username_and_email", "username");
moveFieldAfter("username_and_email", "email");
moveFieldBefore("current_password", "password");
moveFieldAfter("password", "current_password");
moveFieldAfter("password_again", "password");
// Sets visibility condition and validation flags for each field
var gPositiveValidation = !!AccountsTemplates.options.positiveValidation;
var gNegativeValidation = !!AccountsTemplates.options.negativeValidation;
var gShowValidating = !!AccountsTemplates.options.showValidating;
var gContinuousValidation = !!AccountsTemplates.options.continuousValidation;
var gNegativeFeedback = !!AccountsTemplates.options.negativeFeedback;
var gPositiveFeedback = !!AccountsTemplates.options.positiveFeedback;
_.each(this._fields, function(field) {
// Visibility
switch(field._id) {
case "current_password":
field.visible = ["changePwd"];
break;
case "email":
field.visible = ["forgotPwd", "signUp", "resendVerificationEmail"];
if (AccountsTemplates.loginType() === "email") {
field.visible.push("signIn");
}
break;
case "password":
field.visible = ["changePwd", "enrollAccount", "resetPwd", "signIn", "signUp"];
break;
case "password_again":
field.visible = ["changePwd", "enrollAccount", "resetPwd", "signUp"];
break;
case "username":
field.visible = ["signUp"];
if (AccountsTemplates.loginType() === "username") {
field.visible.push("signIn");
}
break;
case "username_and_email":
field.visible = [];
if (AccountsTemplates.loginType() === "username_and_email") {
field.visible.push("signIn");
}
break;
default:
field.visible = ["signUp"];
}
// Validation
var positiveValidation = field.positiveValidation;
if (_.isUndefined(positiveValidation)) {
field.positiveValidation = gPositiveValidation;
}
var negativeValidation = field.negativeValidation;
if (_.isUndefined(negativeValidation)) {
field.negativeValidation = gNegativeValidation;
}
field.validation = field.positiveValidation || field.negativeValidation;
if (_.isUndefined(field.continuousValidation)) {
field.continuousValidation = gContinuousValidation;
}
field.continuousValidation = field.validation && field.continuousValidation;
if (_.isUndefined(field.negativeFeedback)) {
field.negativeFeedback = gNegativeFeedback;
}
if (_.isUndefined(field.positiveFeedback)) {
field.positiveFeedback = gPositiveFeedback;
}
field.feedback = field.negativeFeedback || field.positiveFeedback;
// Validating icon
var showValidating = field.showValidating;
if (_.isUndefined(showValidating)) {
field.showValidating = gShowValidating;
}
// Custom Template
if (field.template) {
if (field.template in Template) {
Template[field.template].helpers(AccountsTemplates.atInputHelpers);
} else {
console.warn(
"[UserAccounts] Warning no template " + field.template + " found!"
);
}
}
});
// Initializes reactive states
var form = new ReactiveDict();
form.set("disabled", false);
form.set("state", "signIn");
form.set("result", null);
form.set("error", null);
form.set("message", null);
this.state = {
form: form,
};
// Possibly subscribes to extended user data (to get the list of registered services...)
if (this.options.showAddRemoveServices) {
Meteor.subscribe("userRegisteredServices");
}
//Check that reCaptcha site keys are available and no secret keys visible
if (this.options.showReCaptcha) {
var atSiteKey = null;
var atSecretKey = null;
var settingsSiteKey = null;
var settingsSecretKey = null;
if (AccountsTemplates.options.reCaptcha) {
atSiteKey = AccountsTemplates.options.reCaptcha.siteKey;
atSecretKey = AccountsTemplates.options.reCaptcha.secretKey;
}
if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.reCaptcha) {
settingsSiteKey = Meteor.settings.public.reCaptcha.siteKey;
settingsSecretKey = Meteor.settings.public.reCaptcha.secretKey;
}
if (atSecretKey || settingsSecretKey) {
//erase the secret key
if (atSecretKey) {
AccountsTemplates.options.reCaptcha.secretKey = null;
}
if (settingsSecretKey) {
Meteor.settings.public.reCaptcha.secretKey = null;
}
var loc = atSecretKey ? "User Accounts configuration!" : "Meteor settings!";
throw new Meteor.Error(401, "User Accounts: DANGER - reCaptcha private key leaked to client from " + loc
+ " Provide the key in server settings ONLY.");
}
if (!atSiteKey && !settingsSiteKey) {
throw new Meteor.Error(401, "User Accounts: reCaptcha site key not found! Please provide it or set showReCaptcha to false.");
}
}
// Marks AccountsTemplates as initialized
this._initialized = true;
};
AT.prototype.linkClick = function(route) {
if (AccountsTemplates.disabled()) {
return;
}
AccountsTemplates.setState(route);
if (AccountsTemplates.options.focusFirstInput) {
var firstVisibleInput = _.find(this.getFields(), function(f) {
return _.contains(f.visible, route);
});
if (firstVisibleInput) {
$("input#at-field-" + firstVisibleInput._id).focus();
}
}
};
AT.prototype.logout = function() {
var onLogoutHook = AccountsTemplates.options.onLogoutHook;
Meteor.logout(function() {
if (onLogoutHook) {
onLogoutHook();
}
});
};
AT.prototype.submitCallback = function(error, state, onSuccess) {
var onSubmitHook = AccountsTemplates.options.onSubmitHook;
if (onSubmitHook) {
onSubmitHook(error, state);
}
if (error) {
if (_.isObject(error.details)) {
// If error.details is an object, we may try to set fields errors from it
_.each(error.details, function(error, fieldId) {
AccountsTemplates.getField(fieldId).setError(error);
});
} else {
var err = "error.accounts.Unknown error";
if (error.reason) {
err = error.reason;
}
if (err.substring(0, 15) !== "error.accounts.") {
err = "error.accounts." + err;
}
AccountsTemplates.state.form.set("error", [err]);
}
AccountsTemplates.setDisabled(false);
// Possibly resets reCaptcha form
if (state === "signUp" && AccountsTemplates.options.showReCaptcha) {
grecaptcha.reset();
}
} else {
if (onSuccess) {
onSuccess();
}
if (state) {
AccountsTemplates.setDisabled(false);
}
}
};
AccountsTemplates = new AT();
// Initialization
Meteor.startup(function() {
AccountsTemplates._init();
});

View file

@ -1,593 +0,0 @@
// ---------------------------------------------------------------------------------
// Patterns for methods" parameters
// ---------------------------------------------------------------------------------
STATE_PAT = {
changePwd: Match.Optional(String),
enrollAccount: Match.Optional(String),
forgotPwd: Match.Optional(String),
resetPwd: Match.Optional(String),
signIn: Match.Optional(String),
signUp: Match.Optional(String),
verifyEmail: Match.Optional(String),
resendVerificationEmail: Match.Optional(String),
};
ERRORS_PAT = {
accountsCreationDisabled: Match.Optional(String),
cannotRemoveService: Match.Optional(String),
captchaVerification: Match.Optional(String),
loginForbidden: Match.Optional(String),
mustBeLoggedIn: Match.Optional(String),
pwdMismatch: Match.Optional(String),
validationErrors: Match.Optional(String),
verifyEmailFirst: Match.Optional(String),
};
INFO_PAT = {
emailSent: Match.Optional(String),
emailVerified: Match.Optional(String),
pwdChanged: Match.Optional(String),
pwdReset: Match.Optional(String),
pwdSet: Match.Optional(String),
signUpVerifyEmail: Match.Optional(String),
verificationEmailSent: Match.Optional(String),
};
INPUT_ICONS_PAT = {
hasError: Match.Optional(String),
hasSuccess: Match.Optional(String),
isValidating: Match.Optional(String),
};
ObjWithStringValues = Match.Where(function (x) {
check(x, Object);
_.each(_.values(x), function(value) {
check(value, String);
});
return true;
});
TEXTS_PAT = {
button: Match.Optional(STATE_PAT),
errors: Match.Optional(ERRORS_PAT),
info: Match.Optional(INFO_PAT),
inputIcons: Match.Optional(INPUT_ICONS_PAT),
maxAllowedLength: Match.Optional(String),
minRequiredLength: Match.Optional(String),
navSignIn: Match.Optional(String),
navSignOut: Match.Optional(String),
optionalField: Match.Optional(String),
pwdLink_link: Match.Optional(String),
pwdLink_pre: Match.Optional(String),
pwdLink_suff: Match.Optional(String),
requiredField: Match.Optional(String),
resendVerificationEmailLink_pre: Match.Optional(String),
resendVerificationEmailLink_link: Match.Optional(String),
resendVerificationEmailLink_suff: Match.Optional(String),
sep: Match.Optional(String),
signInLink_link: Match.Optional(String),
signInLink_pre: Match.Optional(String),
signInLink_suff: Match.Optional(String),
signUpLink_link: Match.Optional(String),
signUpLink_pre: Match.Optional(String),
signUpLink_suff: Match.Optional(String),
socialAdd: Match.Optional(String),
socialConfigure: Match.Optional(String),
socialIcons: Match.Optional(ObjWithStringValues),
socialRemove: Match.Optional(String),
socialSignIn: Match.Optional(String),
socialSignUp: Match.Optional(String),
socialWith: Match.Optional(String),
termsAnd: Match.Optional(String),
termsPreamble: Match.Optional(String),
termsPrivacy: Match.Optional(String),
termsTerms: Match.Optional(String),
title: Match.Optional(STATE_PAT),
};
// Configuration pattern to be checked with check
CONFIG_PAT = {
// Behaviour
confirmPassword: Match.Optional(Boolean),
defaultState: Match.Optional(String),
enablePasswordChange: Match.Optional(Boolean),
enforceEmailVerification: Match.Optional(Boolean),
focusFirstInput: Match.Optional(Boolean),
forbidClientAccountCreation: Match.Optional(Boolean),
lowercaseUsername: Match.Optional(Boolean),
overrideLoginErrors: Match.Optional(Boolean),
sendVerificationEmail: Match.Optional(Boolean),
socialLoginStyle: Match.Optional(Match.OneOf("popup", "redirect")),
// Appearance
defaultLayout: Match.Optional(String),
hideSignInLink: Match.Optional(Boolean),
hideSignUpLink: Match.Optional(Boolean),
showAddRemoveServices: Match.Optional(Boolean),
showForgotPasswordLink: Match.Optional(Boolean),
showResendVerificationEmailLink: Match.Optional(Boolean),
showLabels: Match.Optional(Boolean),
showPlaceholders: Match.Optional(Boolean),
// Client-side Validation
continuousValidation: Match.Optional(Boolean),
negativeFeedback: Match.Optional(Boolean),
negativeValidation: Match.Optional(Boolean),
positiveFeedback: Match.Optional(Boolean),
positiveValidation: Match.Optional(Boolean),
showValidating: Match.Optional(Boolean),
// Privacy Policy and Terms of Use
privacyUrl: Match.Optional(String),
termsUrl: Match.Optional(String),
// Redirects
homeRoutePath: Match.Optional(String),
redirectTimeout: Match.Optional(Number),
// Hooks
onLogoutHook: Match.Optional(Function),
onSubmitHook: Match.Optional(Function),
preSignUpHook: Match.Optional(Function),
postSignUpHook: Match.Optional(Function),
texts: Match.Optional(TEXTS_PAT),
//reCaptcha config
reCaptcha: Match.Optional({
data_type: Match.Optional(Match.OneOf("audio", "image")),
secretKey: Match.Optional(String),
siteKey: Match.Optional(String),
theme: Match.Optional(Match.OneOf("dark", "light")),
}),
showReCaptcha: Match.Optional(Boolean),
};
FIELD_SUB_PAT = {
"default": Match.Optional(String),
changePwd: Match.Optional(String),
enrollAccount: Match.Optional(String),
forgotPwd: Match.Optional(String),
resetPwd: Match.Optional(String),
signIn: Match.Optional(String),
signUp: Match.Optional(String),
};
// Field pattern
FIELD_PAT = {
_id: String,
type: String,
required: Match.Optional(Boolean),
displayName: Match.Optional(Match.OneOf(String, Match.Where(_.isFunction), FIELD_SUB_PAT)),
placeholder: Match.Optional(Match.OneOf(String, FIELD_SUB_PAT)),
select: Match.Optional([{text: String, value: Match.Any}]),
minLength: Match.Optional(Match.Integer),
maxLength: Match.Optional(Match.Integer),
re: Match.Optional(RegExp),
func: Match.Optional(Match.Where(_.isFunction)),
errStr: Match.Optional(String),
// Client-side Validation
continuousValidation: Match.Optional(Boolean),
negativeFeedback: Match.Optional(Boolean),
negativeValidation: Match.Optional(Boolean),
positiveValidation: Match.Optional(Boolean),
positiveFeedback: Match.Optional(Boolean),
// Transforms
trim: Match.Optional(Boolean),
lowercase: Match.Optional(Boolean),
uppercase: Match.Optional(Boolean),
transform: Match.Optional(Match.Where(_.isFunction)),
// Custom options
options: Match.Optional(Object),
template: Match.Optional(String),
};
// -----------------------------------------------------------------------------
// AccountsTemplates object
// -----------------------------------------------------------------------------
// -------------------
// Client/Server stuff
// -------------------
// Constructor
AT = function() {
};
AT.prototype.CONFIG_PAT = CONFIG_PAT;
/*
Each field object is represented by the following properties:
_id: String (required) // A unique field"s id / name
type: String (required) // Displayed input type
required: Boolean (optional) // Specifies Whether to fail or not when field is left empty
displayName: String (optional) // The field"s name to be displayed as a label above the input element
placeholder: String (optional) // The placeholder text to be displayed inside the input element
minLength: Integer (optional) // Possibly specifies the minimum allowed length
maxLength: Integer (optional) // Possibly specifies the maximum allowed length
re: RegExp (optional) // Regular expression for validation
func: Function (optional) // Custom function for validation
errStr: String (optional) // Error message to be displayed in case re validation fails
*/
// Allowed input types
AT.prototype.INPUT_TYPES = [
"checkbox",
"email",
"hidden",
"password",
"radio",
"select",
"tel",
"text",
"url",
];
// Current configuration values
AT.prototype.options = {
// Appearance
//defaultLayout: undefined,
showAddRemoveServices: false,
showForgotPasswordLink: false,
showResendVerificationEmailLink: false,
showLabels: true,
showPlaceholders: true,
// Behaviour
confirmPassword: true,
defaultState: "signIn",
enablePasswordChange: false,
focusFirstInput: !Meteor.isCordova,
forbidClientAccountCreation: false,
lowercaseUsername: false,
overrideLoginErrors: true,
sendVerificationEmail: false,
socialLoginStyle: "popup",
// Client-side Validation
//continuousValidation: false,
//negativeFeedback: false,
//negativeValidation: false,
//positiveValidation: false,
//positiveFeedback: false,
//showValidating: false,
// Privacy Policy and Terms of Use
privacyUrl: undefined,
termsUrl: undefined,
// Hooks
onSubmitHook: undefined,
};
AT.prototype.texts = {
button: {
changePwd: "updateYourPassword",
//enrollAccount: "createAccount",
enrollAccount: "signUp",
forgotPwd: "emailResetLink",
resetPwd: "setPassword",
signIn: "signIn",
signUp: "signUp",
resendVerificationEmail: "Send email again",
},
errors: {
accountsCreationDisabled: "Client side accounts creation is disabled!!!",
cannotRemoveService: "Cannot remove the only active service!",
captchaVerification: "Captcha verification failed!",
loginForbidden: "error.accounts.Login forbidden",
mustBeLoggedIn: "error.accounts.Must be logged in",
pwdMismatch: "error.pwdsDontMatch",
validationErrors: "Validation Errors",
verifyEmailFirst: "Please verify your email first. Check the email and follow the link!",
},
navSignIn: 'signIn',
navSignOut: 'signOut',
info: {
emailSent: "info.emailSent",
emailVerified: "info.emailVerified",
pwdChanged: "info.passwordChanged",
pwdReset: "info.passwordReset",
pwdSet: "Password Set",
signUpVerifyEmail: "Successful Registration! Please check your email and follow the instructions.",
verificationEmailSent: "A new email has been sent to you. If the email doesn't show up in your inbox, be sure to check your spam folder.",
},
inputIcons: {
isValidating: "fa fa-spinner fa-spin",
hasSuccess: "fa fa-check",
hasError: "fa fa-times",
},
maxAllowedLength: "Maximum allowed length",
minRequiredLength: "Minimum required length",
optionalField: "optional",
pwdLink_pre: "",
pwdLink_link: "forgotPassword",
pwdLink_suff: "",
requiredField: "Required Field",
resendVerificationEmailLink_pre: "Verification email lost?",
resendVerificationEmailLink_link: "Send again",
resendVerificationEmailLink_suff: "",
sep: "OR",
signInLink_pre: "ifYouAlreadyHaveAnAccount",
signInLink_link: "signin",
signInLink_suff: "",
signUpLink_pre: "dontHaveAnAccount",
signUpLink_link: "signUp",
signUpLink_suff: "",
socialAdd: "add",
socialConfigure: "configure",
socialIcons: {
"meteor-developer": "fa fa-rocket"
},
socialRemove: "remove",
socialSignIn: "signIn",
socialSignUp: "signUp",
socialWith: "with",
termsPreamble: "clickAgree",
termsPrivacy: "privacyPolicy",
termsAnd: "and",
termsTerms: "terms",
title: {
changePwd: "changePassword",
enrollAccount: "createAccount",
forgotPwd: "resetYourPassword",
resetPwd: "resetYourPassword",
signIn: "signIn",
signUp: "createAccount",
verifyEmail: "",
resendVerificationEmail: "Send the verification email again",
},
};
AT.prototype.SPECIAL_FIELDS = [
"password_again",
"username_and_email",
];
// SignIn / SignUp fields
AT.prototype._fields = [
new Field({
_id: "email",
type: "email",
required: true,
lowercase: true,
trim: true,
func: function(email) {
return !_.contains(email, '@');
},
errStr: 'Invalid email',
}),
new Field({
_id: "password",
type: "password",
required: true,
minLength: 6,
displayName: {
"default": "password",
changePwd: "newPassword",
resetPwd: "newPassword",
},
placeholder: {
"default": "password",
changePwd: "newPassword",
resetPwd: "newPassword",
},
}),
];
AT.prototype._initialized = false;
// Input type validation
AT.prototype._isValidInputType = function(value) {
return _.indexOf(this.INPUT_TYPES, value) !== -1;
};
AT.prototype.addField = function(field) {
// Fields can be added only before initialization
if (this._initialized) {
throw new Error("AccountsTemplates.addField should strictly be called before AccountsTemplates.init!");
}
field = _.pick(field, _.keys(FIELD_PAT));
check(field, FIELD_PAT);
// Checks there"s currently no field called field._id
if (_.indexOf(_.pluck(this._fields, "_id"), field._id) !== -1) {
throw new Error("A field called " + field._id + " already exists!");
}
// Validates field.type
if (!this._isValidInputType(field.type)) {
throw new Error("field.type is not valid!");
}
// Checks field.minLength is strictly positive
if (typeof field.minLength !== "undefined" && field.minLength <= 0) {
throw new Error("field.minLength should be greater than zero!");
}
// Checks field.maxLength is strictly positive
if (typeof field.maxLength !== "undefined" && field.maxLength <= 0) {
throw new Error("field.maxLength should be greater than zero!");
}
// Checks field.maxLength is greater than field.minLength
if (typeof field.minLength !== "undefined" && typeof field.minLength !== "undefined" && field.maxLength < field.minLength) {
throw new Error("field.maxLength should be greater than field.maxLength!");
}
if (!(Meteor.isServer && _.contains(this.SPECIAL_FIELDS, field._id))) {
this._fields.push(new Field(field));
}
return this._fields;
};
AT.prototype.addFields = function(fields) {
var ok;
try { // don"t bother with `typeof` - just access `length` and `catch`
ok = fields.length > 0 && "0" in Object(fields);
} catch (e) {
throw new Error("field argument should be an array of valid field objects!");
}
if (ok) {
_.map(fields, function(field) {
this.addField(field);
}, this);
} else {
throw new Error("field argument should be an array of valid field objects!");
}
return this._fields;
};
AT.prototype.configure = function(config) {
// Configuration options can be set only before initialization
if (this._initialized) {
throw new Error("Configuration options must be set before AccountsTemplates.init!");
}
// Updates the current configuration
check(config, CONFIG_PAT);
var options = _.omit(config, "texts", "reCaptcha");
this.options = _.defaults(options, this.options);
// Possibly sets up reCaptcha options
var reCaptcha = config.reCaptcha;
if (reCaptcha) {
// Updates the current button object
this.options.reCaptcha = _.defaults(reCaptcha, this.options.reCaptcha || {});
}
// Possibly sets up texts...
if (config.texts) {
var texts = config.texts;
var simpleTexts = _.omit(texts, "button", "errors", "info", "inputIcons", "socialIcons", "title");
this.texts = _.defaults(simpleTexts, this.texts);
if (texts.button) {
// Updates the current button object
this.texts.button = _.defaults(texts.button, this.texts.button);
}
if (texts.errors) {
// Updates the current errors object
this.texts.errors = _.defaults(texts.errors, this.texts.errors);
}
if (texts.info) {
// Updates the current info object
this.texts.info = _.defaults(texts.info, this.texts.info);
}
if (texts.inputIcons) {
// Updates the current inputIcons object
this.texts.inputIcons = _.defaults(texts.inputIcons, this.texts.inputIcons);
}
if (texts.socialIcons) {
// Updates the current socialIcons object
this.texts.socialIcons = _.defaults(texts.socialIcons, this.texts.socialIcons);
}
if (texts.title) {
// Updates the current title object
this.texts.title = _.defaults(texts.title, this.texts.title);
}
}
};
AT.prototype.configureRoute = function(route, options) {
console.warn('You now need a routing package like useraccounts:iron-routing or useraccounts:flow-routing to be able to configure routes!');
};
AT.prototype.hasField = function(fieldId) {
return !!this.getField(fieldId);
};
AT.prototype.getField = function(fieldId) {
var field = _.filter(this._fields, function(field) {
return field._id === fieldId;
});
return (field.length === 1) ? field[0] : undefined;
};
AT.prototype.getFields = function() {
return this._fields;
};
AT.prototype.getFieldIds = function() {
return _.pluck(this._fields, "_id");
};
AT.prototype.getRoutePath = function(route) {
return "#";
};
AT.prototype.oauthServices = function() {
// Extracts names of available services
var names;
if (Meteor.isServer) {
names = (Accounts.oauth && Accounts.oauth.serviceNames()) || [];
} else {
names = (Accounts.oauth && Accounts.loginServicesConfigured() && Accounts.oauth.serviceNames()) || [];
}
// Extracts names of configured services
var configuredServices = [];
if (Accounts.loginServiceConfiguration) {
configuredServices = _.pluck(Accounts.loginServiceConfiguration.find().fetch(), "service");
}
// Builds a list of objects containing service name as _id and its configuration status
var services = _.map(names, function(name) {
return {
_id : name,
configured: _.contains(configuredServices, name),
};
});
// Checks whether there is a UI to configure services...
// XXX: this only works with the accounts-ui package
var showUnconfigured = typeof Accounts._loginButtonsSession !== "undefined";
// Filters out unconfigured services in case they"re not to be displayed
if (!showUnconfigured) {
services = _.filter(services, function(service) {
return service.configured;
});
}
// Sorts services by name
services = _.sortBy(services, function(service) {
return service._id;
});
return services;
};
AT.prototype.removeField = function(fieldId) {
// Fields can be removed only before initialization
if (this._initialized) {
throw new Error("AccountsTemplates.removeField should strictly be called before AccountsTemplates.init!");
}
// Tries to look up the field with given _id
var index = _.indexOf(_.pluck(this._fields, "_id"), fieldId);
if (index !== -1) {
return this._fields.splice(index, 1)[0];
} else if (!(Meteor.isServer && _.contains(this.SPECIAL_FIELDS, fieldId))) {
throw new Error("A field called " + fieldId + " does not exist!");
}
};

View file

@ -1,292 +0,0 @@
// ---------------------------------------------------------------------------------
// Field object
// ---------------------------------------------------------------------------------
Field = function(field) {
check(field, FIELD_PAT);
_.defaults(this, field);
this.validating = new ReactiveVar(false);
this.status = new ReactiveVar(null);
};
if (Meteor.isClient) {
Field.prototype.clearStatus = function() {
return this.status.set(null);
};
}
if (Meteor.isServer) {
Field.prototype.clearStatus = function() {
// Nothing to do server-side
return;
};
}
Field.prototype.fixValue = function(value) {
if (this.type === "checkbox") {
return !!value;
}
if (this.type === "select") {
// TODO: something working...
return value;
}
if (this.type === "radio") {
// TODO: something working...
return value;
}
// Possibly applies required transformations to the input value
if (this.trim) {
value = value.trim();
}
if (this.lowercase) {
value = value.toLowerCase();
}
if (this.uppercase) {
value = value.toUpperCase();
}
if (!!this.transform) {
value = this.transform(value);
}
return value;
};
if (Meteor.isClient) {
Field.prototype.getDisplayName = function(state) {
var displayName = this.displayName;
if (_.isFunction(displayName)) {
displayName = displayName();
} else if (_.isObject(displayName)) {
displayName = displayName[state] || displayName["default"];
}
if (!displayName) {
displayName = capitalize(this._id);
}
return displayName;
};
}
if (Meteor.isClient) {
Field.prototype.getPlaceholder = function(state) {
var placeholder = this.placeholder;
if (_.isObject(placeholder)) {
placeholder = placeholder[state] || placeholder["default"];
}
if (!placeholder) {
placeholder = capitalize(this._id);
}
return placeholder;
};
}
Field.prototype.getStatus = function() {
return this.status.get();
};
if (Meteor.isClient) {
Field.prototype.getValue = function(templateInstance) {
if (this.type === "checkbox") {
return !!(templateInstance.$("#at-field-" + this._id + ":checked").val());
}
if (this.type === "radio") {
return templateInstance.$("[name=at-field-"+ this._id + "]:checked").val();
}
return templateInstance.$("#at-field-" + this._id).val();
};
}
if (Meteor.isClient) {
Field.prototype.hasError = function() {
return this.negativeValidation && this.status.get();
};
}
if (Meteor.isClient) {
Field.prototype.hasIcon = function() {
if (this.showValidating && this.isValidating()) {
return true;
}
if (this.negativeFeedback && this.hasError()) {
return true;
}
if (this.positiveFeedback && this.hasSuccess()) {
return true;
}
};
}
if (Meteor.isClient) {
Field.prototype.hasSuccess = function() {
return this.positiveValidation && this.status.get() === false;
};
}
if (Meteor.isClient)
Field.prototype.iconClass = function() {
if (this.isValidating()) {
return AccountsTemplates.texts.inputIcons["isValidating"];
}
if (this.hasError()) {
return AccountsTemplates.texts.inputIcons["hasError"];
}
if (this.hasSuccess()) {
return AccountsTemplates.texts.inputIcons["hasSuccess"];
}
};
if (Meteor.isClient) {
Field.prototype.isValidating = function() {
return this.validating.get();
};
}
if (Meteor.isClient) {
Field.prototype.setError = function(err) {
check(err, Match.OneOf(String, undefined, Boolean));
if (err === false) {
return this.status.set(false);
}
return this.status.set(err || true);
};
}
if (Meteor.isServer) {
Field.prototype.setError = function(err) {
// Nothing to do server-side
return;
};
}
if (Meteor.isClient) {
Field.prototype.setSuccess = function() {
return this.status.set(false);
};
}
if (Meteor.isServer) {
Field.prototype.setSuccess = function() {
// Nothing to do server-side
return;
};
}
if (Meteor.isClient) {
Field.prototype.setValidating = function(state) {
check(state, Boolean);
return this.validating.set(state);
};
}
if (Meteor.isServer) {
Field.prototype.setValidating = function(state) {
// Nothing to do server-side
return;
};
}
if (Meteor.isClient) {
Field.prototype.setValue = function(templateInstance, value) {
if (this.type === "checkbox") {
templateInstance.$("#at-field-" + this._id).prop('checked', true);
return;
}
if (this.type === "radio") {
templateInstance.$("[name=at-field-"+ this._id + "]").prop('checked', true);
return;
}
templateInstance.$("#at-field-" + this._id).val(value);
};
}
Field.prototype.validate = function(value, strict) {
check(value, Match.OneOf(undefined, String, Boolean));
this.setValidating(true);
this.clearStatus();
if (_.isUndefined(value) || value === '') {
if (!!strict) {
if (this.required) {
this.setError(AccountsTemplates.texts.requiredField);
this.setValidating(false);
return AccountsTemplates.texts.requiredField;
} else {
this.setSuccess();
this.setValidating(false);
return false;
}
} else {
this.clearStatus();
this.setValidating(false);
return null;
}
}
var valueLength = value.length;
var minLength = this.minLength;
if (minLength && valueLength < minLength) {
this.setError(AccountsTemplates.texts.minRequiredLength + ": " + minLength);
this.setValidating(false);
return AccountsTemplates.texts.minRequiredLength + ": " + minLength;
}
var maxLength = this.maxLength;
if (maxLength && valueLength > maxLength) {
this.setError(AccountsTemplates.texts.maxAllowedLength + ": " + maxLength);
this.setValidating(false);
return AccountsTemplates.texts.maxAllowedLength + ": " + maxLength;
}
if (this.re && valueLength && !value.match(this.re)) {
this.setError(this.errStr);
this.setValidating(false);
return this.errStr;
}
if (this.func) {
var result = this.func(value);
var err = result === true ? this.errStr || true : result;
if (_.isUndefined(result)) {
return err;
}
this.status.set(err);
this.setValidating(false);
return err;
}
this.setSuccess();
this.setValidating(false);
return false;
};

View file

@ -1,25 +0,0 @@
/* global
AccountsTemplates: false
*/
"use strict";
Meteor.methods({
ATRemoveService: function(serviceName) {
check(serviceName, String);
var userId = this.userId;
if (userId) {
var user = Meteor.users.findOne(userId);
var numServices = _.keys(user.services).length; // including "resume"
var unset = {};
if (numServices === 2) {
throw new Meteor.Error(403, AccountsTemplates.texts.errors.cannotRemoveService, {});
}
unset["services." + serviceName] = "";
Meteor.users.update(userId, {$unset: unset});
}
},
});

View file

@ -1,184 +0,0 @@
/* global
AT: false,
AccountsTemplates: false
*/
"use strict";
// Initialization
AT.prototype.init = function() {
console.warn("[AccountsTemplates] There is no more need to call AccountsTemplates.init()! Simply remove the call ;-)");
};
AT.prototype._init = function() {
if (this._initialized) {
return;
}
// Checks there is at least one account service installed
if (!Package["accounts-password"] && (!Accounts.oauth || Accounts.oauth.serviceNames().length === 0)) {
throw Error("AccountsTemplates: You must add at least one account service!");
}
// A password field is strictly required
var password = this.getField("password");
if (!password) {
throw Error("A password field is strictly required!");
}
if (password.type !== "password") {
throw Error("The type of password field should be password!");
}
// Then we can have "username" or "email" or even both of them
// but at least one of the two is strictly required
var username = this.getField("username");
var email = this.getField("email");
if (!username && !email) {
throw Error("At least one field out of username and email is strictly required!");
}
if (username && !username.required) {
throw Error("The username field should be required!");
}
if (email) {
if (email.type !== "email") {
throw Error("The type of email field should be email!");
}
if (username) {
// username and email
if (username.type !== "text") {
throw Error("The type of username field should be text when email field is present!");
}
} else {
// email only
if (!email.required) {
throw Error("The email field should be required when username is not present!");
}
}
} else {
// username only
if (username.type !== "text" && username.type !== "tel") {
throw Error("The type of username field should be text or tel!");
}
}
// Possibly publish more user data in order to be able to show add/remove
// buttons for 3rd-party services
if (this.options.showAddRemoveServices) {
// Publish additional current user info to get the list of registered services
// XXX TODO: use
// Accounts.addAutopublishFields({
// forLoggedInUser: ['services.facebook'],
// forOtherUsers: [],
// })
// ...adds only user.services.*.id
Meteor.publish("userRegisteredServices", function() {
var userId = this.userId;
return Meteor.users.find(userId, {fields: {services: 1}});
/*
if (userId) {
var user = Meteor.users.findOne(userId);
var services_id = _.chain(user.services)
.keys()
.reject(function(service) {return service === "resume";})
.map(function(service) {return "services." + service + ".id";})
.value();
var projection = {};
_.each(services_id, function(key) {projection[key] = 1;});
return Meteor.users.find(userId, {fields: projection});
}
*/
});
}
// Security stuff
if (this.options.overrideLoginErrors) {
Accounts.validateLoginAttempt(function(attempt) {
if (attempt.error) {
var reason = attempt.error.reason;
if (reason === "User not found" || reason === "Incorrect password") {
throw new Meteor.Error(403, AccountsTemplates.texts.errors.loginForbidden);
}
}
return attempt.allowed;
});
}
if (this.options.sendVerificationEmail && this.options.enforceEmailVerification) {
Accounts.validateLoginAttempt(function(attempt) {
if (!attempt.allowed) {
return false;
}
if (attempt.type !== "password" || attempt.methodName !== "login") {
return attempt.allowed;
}
var user = attempt.user;
if (!user) {
return attempt.allowed;
}
var ok = true;
var loginEmail = attempt.methodArguments[0].user.email.toLowerCase();
if (loginEmail) {
var email = _.filter(user.emails, function(obj) {
return obj.address.toLowerCase() === loginEmail;
});
if (!email.length || !email[0].verified) {
ok = false;
}
} else {
// we got the username, lets check there's at lease one verified email
var emailVerified = _.chain(user.emails)
.pluck('verified')
.any()
.value();
if (!emailVerified) {
ok = false;
}
}
if (!ok) {
throw new Meteor.Error(401, AccountsTemplates.texts.errors.verifyEmailFirst);
}
return attempt.allowed;
});
}
//Check that reCaptcha secret keys are available
if (this.options.showReCaptcha) {
var atSecretKey = AccountsTemplates.options.reCaptcha && AccountsTemplates.options.reCaptcha.secretKey;
var settingsSecretKey = Meteor.settings.reCaptcha && Meteor.settings.reCaptcha.secretKey;
if (!atSecretKey && !settingsSecretKey) {
throw new Meteor.Error(401, "User Accounts: reCaptcha secret key not found! Please provide it or set showReCaptcha to false." );
}
}
// Marks AccountsTemplates as initialized
this._initialized = true;
};
AccountsTemplates = new AT();
// Client side account creation is disabled by default:
// the methos ATCreateUserServer is used instead!
// to actually disable client side account creation use:
//
// AccountsTemplates.config({
// forbidClientAccountCreation: true
// });
Accounts.config({
forbidClientAccountCreation: true
});
// Initialization
Meteor.startup(function() {
AccountsTemplates._init();
});

View file

@ -1,142 +0,0 @@
/* global
AccountsTemplates
*/
"use strict";
Meteor.methods({
ATCreateUserServer: function(options) {
if (AccountsTemplates.options.forbidClientAccountCreation) {
throw new Meteor.Error(403, AccountsTemplates.texts.errors.accountsCreationDisabled);
}
// createUser() does more checking.
check(options, Object);
var allFieldIds = AccountsTemplates.getFieldIds();
// Picks-up whitelisted fields for profile
var profile = options.profile;
profile = _.pick(profile, allFieldIds);
profile = _.omit(profile, "username", "email", "password");
// Validates fields" value
var signupInfo = _.clone(profile);
if (options.username) {
signupInfo.username = options.username;
if (AccountsTemplates.options.lowercaseUsername) {
signupInfo.username = signupInfo.username.trim().replace(/\s+/gm, ' ');
options.profile.name = signupInfo.username;
signupInfo.username = signupInfo.username.toLowerCase().replace(/\s+/gm, '');
options.username = signupInfo.username;
}
}
if (options.email) {
signupInfo.email = options.email;
if (AccountsTemplates.options.lowercaseUsername) {
signupInfo.email = signupInfo.email.toLowerCase().replace(/\s+/gm, '');
options.email = signupInfo.email;
}
}
if (options.password) {
signupInfo.password = options.password;
}
var validationErrors = {};
var someError = false;
// Validates fields values
_.each(AccountsTemplates.getFields(), function(field) {
var fieldId = field._id;
var value = signupInfo[fieldId];
if (fieldId === "password") {
// Can"t Pick-up password here
// NOTE: at this stage the password is already encripted,
// so there is no way to validate it!!!
check(value, Object);
return;
}
var validationErr = field.validate(value, "strict");
if (validationErr) {
validationErrors[fieldId] = validationErr;
someError = true;
}
});
if (AccountsTemplates.options.showReCaptcha) {
var secretKey = null;
if (AccountsTemplates.options.reCaptcha && AccountsTemplates.options.reCaptcha.secretKey) {
secretKey = AccountsTemplates.options.reCaptcha.secretKey;
} else {
secretKey = Meteor.settings.reCaptcha.secretKey;
}
var apiResponse = HTTP.post("https://www.google.com/recaptcha/api/siteverify", {
params: {
secret: secretKey,
response: options.profile.reCaptchaResponse,
remoteip: this.connection.clientAddress,
}
}).data;
if (!apiResponse.success) {
throw new Meteor.Error(403, AccountsTemplates.texts.errors.captchaVerification,
apiResponse['error-codes'] ? apiResponse['error-codes'].join(", ") : "Unknown Error.");
}
}
if (someError) {
throw new Meteor.Error(403, AccountsTemplates.texts.errors.validationErrors, validationErrors);
}
// Possibly removes the profile field
if (_.isEmpty(options.profile)) {
delete options.profile;
}
// Create user. result contains id and token.
var userId = Accounts.createUser(options);
// safety belt. createUser is supposed to throw on error. send 500 error
// instead of sending a verification email with empty userid.
if (! userId) {
throw new Error("createUser failed to insert new user");
}
// Call postSignUpHook, if any...
var postSignUpHook = AccountsTemplates.options.postSignUpHook;
if (postSignUpHook) {
postSignUpHook(userId, options);
}
// Send a email address verification email in case the context permits it
// and the specific configuration flag was set to true
if (options.email && AccountsTemplates.options.sendVerificationEmail) {
Accounts.sendVerificationEmail(userId, options.email);
}
},
// Resend a user's verification e-mail
ATResendVerificationEmail: function (email) {
check(email, String);
var user = Meteor.users.findOne({ "emails.address": email });
// Send the standard error back to the client if no user exist with this e-mail
if (!user) {
throw new Meteor.Error(403, "User not found");
}
try {
Accounts.sendVerificationEmail(user._id);
} catch (error) {
// Handle error when email already verified
// https://github.com/dwinston/send-verification-email-bug
throw new Meteor.Error(403, "Already verified");
}
},
});

View file

@ -1,26 +0,0 @@
AT.prototype.atErrorHelpers = {
singleError: function() {
var errors = AccountsTemplates.state.form.get("error");
return errors && errors.length === 1;
},
error: function() {
return AccountsTemplates.state.form.get("error");
},
errorText: function(){
var field, err;
if (this.field){
field = T9n.get(this.field, markIfMissing=false);
err = T9n.get(this.err, markIfMissing=false);
}
else
err = T9n.get(this.valueOf(), markIfMissing=false);
// Possibly removes initial prefix in case the key in not found inside t9n
if (err.substring(0, 15) === "error.accounts.")
err = err.substring(15);
if (field)
return field + ": " + err;
return err;
},
};

View file

@ -1,83 +0,0 @@
AT.prototype.atFormHelpers = {
hide: function(){
var state = this.state || AccountsTemplates.getState();
return state === "hide";
},
showTitle: function(next_state){
var state = next_state || this.state || AccountsTemplates.getState();
if (Meteor.userId() && state === "signIn")
return false;
return !!AccountsTemplates.texts.title[state];
},
showOauthServices: function(next_state){
var state = next_state || this.state || AccountsTemplates.getState();
if (!(state === "signIn" || state === "signUp"))
return false;
var services = AccountsTemplates.oauthServices();
if (!services.length)
return false;
if (Meteor.userId())
return AccountsTemplates.options.showAddRemoveServices;
return true;
},
showServicesSeparator: function(next_state){
var pwdService = Package["accounts-password"] !== undefined;
var state = next_state || this.state || AccountsTemplates.getState();
var rightState = (state === "signIn" || state === "signUp");
return rightState && !Meteor.userId() && pwdService && AccountsTemplates.oauthServices().length;
},
showError: function(next_state) {
return !!AccountsTemplates.state.form.get("error");
},
showResult: function(next_state) {
return !!AccountsTemplates.state.form.get("result");
},
showMessage: function(next_state) {
return !!AccountsTemplates.state.form.get("message");
},
showPwdForm: function(next_state) {
if (Package["accounts-password"] === undefined)
return false;
var state = next_state || this.state || AccountsTemplates.getState();
if ((state === "verifyEmail") || (state === "signIn" && Meteor.userId()))
return false;
return true;
},
showSignInLink: function(next_state){
if (AccountsTemplates.options.hideSignInLink)
return false;
var state = next_state || this.state || AccountsTemplates.getState();
if (AccountsTemplates.options.forbidClientAccountCreation && state === "forgotPwd")
return true;
return state === "signUp";
},
showSignUpLink: function(next_state){
if (AccountsTemplates.options.hideSignUpLink)
return false;
var state = next_state || this.state || AccountsTemplates.getState();
return ((state === "signIn" && !Meteor.userId()) || state === "forgotPwd") && !AccountsTemplates.options.forbidClientAccountCreation;
},
showTermsLink: function(next_state){
//TODO: Add privacyRoute and termsRoute as alternatives (the point of named routes is
// being able to change the url in one place only)
if (!!AccountsTemplates.options.privacyUrl || !!AccountsTemplates.options.termsUrl) {
var state = next_state || this.state || AccountsTemplates.getState();
if (state === "signUp" || state === "enrollAccount" ) {
return true;
}
}
/*
if (state === "signIn"){
var pwdService = Package["accounts-password"] !== undefined;
if (!pwdService)
return true;
}
*/
return false;
},
showResendVerificationEmailLink: function(){
var parentData = Template.currentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
return (state === "signIn" || state === "forgotPwd") && AccountsTemplates.options.showResendVerificationEmailLink;
},
};

View file

@ -1,124 +0,0 @@
AT.prototype.atInputRendered = [function(){
var fieldId = this.data._id;
var parentData = Template.currentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
if (AccountsTemplates.options.focusFirstInput) {
var firstVisibleInput = _.find(AccountsTemplates.getFields(), function(f){
return _.contains(f.visible, state);
});
if (firstVisibleInput && firstVisibleInput._id === fieldId) {
this.$("input#at-field-" + fieldId).focus();
}
}
}];
AT.prototype.atInputHelpers = {
disabled: function() {
return AccountsTemplates.disabled();
},
showLabels: function() {
return AccountsTemplates.options.showLabels;
},
displayName: function() {
var parentData = Template.parentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
var displayName = this.getDisplayName(state);
return T9n.get(displayName, markIfMissing=false);
},
optionalText: function(){
return "(" + T9n.get(AccountsTemplates.texts.optionalField, markIfMissing=false) + ")";
},
templateName: function() {
if (this.template)
return this.template;
if (this.type === "checkbox")
return "atCheckboxInput";
if (this.type === "select")
return "atSelectInput";
if (this.type === "radio")
return "atRadioInput";
if (this.type === "hidden")
return "atHiddenInput";
return "atTextInput";
},
values: function(){
var id = this._id;
return _.map(this.select, function(select){
var s = _.clone(select);
s._id = id + "-" + select.value;
s.id = id;
return s;
});
},
errorText: function() {
var err = this.getStatus();
return T9n.get(err, markIfMissing=false);
},
placeholder: function() {
if (AccountsTemplates.options.showPlaceholders) {
var parentData = Template.parentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
var placeholder = this.getPlaceholder(state);
return T9n.get(placeholder, markIfMissing=false);
}
},
};
AT.prototype.atInputEvents = {
"focusin input": function(event, t){
var field = Template.currentData();
field.clearStatus();
},
"focusout input, change select": function(event, t){
var field = Template.currentData();
var fieldId = field._id;
var rawValue = field.getValue(t);
var value = field.fixValue(rawValue);
// Possibly updates the input value
if (value !== rawValue) {
field.setValue(t, value);
}
// Client-side only validation
if (!field.validation)
return;
var parentData = Template.parentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
// No validation during signIn
if (state === "signIn")
return;
// Special case for password confirmation
if (value && fieldId === "password_again"){
if (value !== $("#at-field-password").val())
return field.setError(AccountsTemplates.texts.errors.pwdMismatch);
}
field.validate(value);
},
"keyup input": function(event, t){
var field = Template.currentData();
// Client-side only continuous validation
if (!field.continuousValidation)
return;
var parentData = Template.parentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
// No validation during signIn
if (state === "signIn")
return;
var fieldId = field._id;
var rawValue = field.getValue(t);
var value = field.fixValue(rawValue);
// Possibly updates the input value
if (value !== rawValue) {
field.setValue(t, value);
}
// Special case for password confirmation
if (value && fieldId === "password_again"){
if (value !== $("#at-field-password").val())
return field.setError(AccountsTemplates.texts.errors.pwdMismatch);
}
field.validate(value);
},
};

View file

@ -1,7 +0,0 @@
AT.prototype.atMessageHelpers = {
message: function() {
var messageText = AccountsTemplates.state.form.get("message");
if (messageText)
return T9n.get(messageText, markIfMissing=false);
},
};

View file

@ -1,16 +0,0 @@
AT.prototype.atNavButtonHelpers = {
text: function(){
var key = Meteor.userId() ? AccountsTemplates.texts.navSignOut : AccountsTemplates.texts.navSignIn;
return T9n.get(key, markIfMissing=false);
}
};
AT.prototype.atNavButtonEvents = {
'click #at-nav-button': function(event){
event.preventDefault();
if (Meteor.userId())
AccountsTemplates.logout();
else
AccountsTemplates.linkClick("signIn");
},
};

View file

@ -1,5 +0,0 @@
AT.prototype.atOauthHelpers = {
oauthService: function() {
return AccountsTemplates.oauthServices();
},
};

View file

@ -1,331 +0,0 @@
AT.prototype.atPwdFormHelpers = {
disabled: function() {
return AccountsTemplates.disabled();
},
fields: function() {
var parentData = Template.currentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
return _.filter(AccountsTemplates.getFields(), function(s) {
return _.contains(s.visible, state);
});
},
showForgotPasswordLink: function() {
var parentData = Template.currentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
return state === "signIn" && AccountsTemplates.options.showForgotPasswordLink;
},
showReCaptcha: function() {
var parentData = Template.currentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
return state === "signUp" && AccountsTemplates.options.showReCaptcha;
},
};
var toLowercaseUsername = function(value){
return value.toLowerCase().replace(/\s+/gm, '');
};
AT.prototype.atPwdFormEvents = {
// Form submit
"submit #at-pwd-form": function(event, t) {
event.preventDefault();
t.$("#at-btn").blur();
AccountsTemplates.setDisabled(true);
var parentData = Template.currentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
var preValidation = (state !== "signIn");
// Client-side pre-validation
// Validates fields values
// NOTE: This is the only place where password validation can be enforced!
var formData = {};
var someError = false;
var errList = [];
_.each(AccountsTemplates.getFields(), function(field){
// Considers only visible fields...
if (!_.contains(field.visible, state))
return;
var fieldId = field._id;
var rawValue = field.getValue(t);
var value = field.fixValue(rawValue);
// Possibly updates the input value
if (value !== rawValue) {
field.setValue(t, value);
}
if (value !== undefined && value !== "") {
formData[fieldId] = value;
}
// Validates the field value only if current state is not "signIn"
if (preValidation && field.getStatus() !== false){
var validationErr = field.validate(value, "strict");
if (validationErr) {
if (field.negativeValidation)
field.setError(validationErr);
else{
var fId = T9n.get(field.getDisplayName(), markIfMissing=false);
//errList.push(fId + ": " + err);
errList.push({
field: field.getDisplayName(),
err: validationErr
});
}
someError = true;
}
else
field.setSuccess();
}
});
// Clears error and result
AccountsTemplates.clearError();
AccountsTemplates.clearResult();
AccountsTemplates.clearMessage();
// Possibly sets errors
if (someError){
if (errList.length)
AccountsTemplates.state.form.set("error", errList);
AccountsTemplates.setDisabled(false);
//reset reCaptcha form
if (state === "signUp" && AccountsTemplates.options.showReCaptcha) {
grecaptcha.reset();
}
return;
}
// Extracts username, email, and pwds
var current_password = formData.current_password;
var email = formData.email;
var password = formData.password;
var password_again = formData.password_again;
var username = formData.username;
var username_and_email = formData.username_and_email;
// Clears profile data removing username, email, and pwd
delete formData.current_password;
delete formData.email;
delete formData.password;
delete formData.password_again;
delete formData.username;
delete formData.username_and_email;
if (AccountsTemplates.options.confirmPassword){
// Checks passwords for correct match
if (password_again && password !== password_again){
var pwd_again = AccountsTemplates.getField("password_again");
if (pwd_again.negativeValidation)
pwd_again.setError(AccountsTemplates.texts.errors.pwdMismatch);
else
AccountsTemplates.state.form.set("error", [{
field: pwd_again.getDisplayName(),
err: AccountsTemplates.texts.errors.pwdMismatch
}]);
AccountsTemplates.setDisabled(false);
//reset reCaptcha form
if (state === "signUp" && AccountsTemplates.options.showReCaptcha) {
grecaptcha.reset();
}
return;
}
}
// -------
// Sign In
// -------
if (state === "signIn") {
var pwdOk = !!password;
var userOk = true;
var loginSelector;
if (email) {
if (AccountsTemplates.options.lowercaseUsername) {
email = toLowercaseUsername(email);
}
loginSelector = {email: email};
}
else if (username) {
if (AccountsTemplates.options.lowercaseUsername) {
username = toLowercaseUsername(username);
}
loginSelector = {username: username};
}
else if (username_and_email) {
if (AccountsTemplates.options.lowercaseUsername) {
username_and_email = toLowercaseUsername(username_and_email);
}
loginSelector = username_and_email;
}
else
userOk = false;
// Possibly exits if not both 'password' and 'username' are non-empty...
if (!pwdOk || !userOk){
AccountsTemplates.state.form.set("error", [AccountsTemplates.texts.errors.loginForbidden]);
AccountsTemplates.setDisabled(false);
return;
}
return Meteor.loginWithPassword(loginSelector, password, function(error) {
AccountsTemplates.submitCallback(error, state);
});
}
// -------
// Sign Up
// -------
if (state === "signUp") {
// Possibly gets reCaptcha response
if (AccountsTemplates.options.showReCaptcha) {
var response = grecaptcha.getResponse();
if (response === "") {
// recaptcha verification has not completed yet (or has expired)...
// ...simply ignore submit event!
AccountsTemplates.setDisabled(false);
return;
} else {
formData.reCaptchaResponse = response;
}
}
var hash = Accounts._hashPassword(password);
var options = {
username: username,
email: email,
password: hash,
profile: formData,
};
// Call preSignUpHook, if any...
var preSignUpHook = AccountsTemplates.options.preSignUpHook;
if (preSignUpHook) {
preSignUpHook(password, options);
}
return Meteor.call("ATCreateUserServer", options, function(error){
if (error && error.reason === 'Email already exists.') {
if (AccountsTemplates.options.showReCaptcha) {
grecaptcha.reset();
}
}
AccountsTemplates.submitCallback(error, undefined, function(){
if (AccountsTemplates.options.sendVerificationEmail && AccountsTemplates.options.enforceEmailVerification){
AccountsTemplates.submitCallback(error, state, function () {
AccountsTemplates.state.form.set("result", AccountsTemplates.texts.info.signUpVerifyEmail);
// Cleans up input fields' content
_.each(AccountsTemplates.getFields(), function(field){
// Considers only visible fields...
if (!_.contains(field.visible, state))
return;
var elem = t.$("#at-field-" + field._id);
// Naïve reset
if (field.type === "checkbox") elem.prop('checked', false);
else elem.val("");
});
AccountsTemplates.setDisabled(false);
AccountsTemplates.avoidRedirect = true;
});
}
else {
var loginSelector;
if (email) {
if (AccountsTemplates.options.lowercaseUsername) {
email = toLowercaseUsername(email);
}
loginSelector = {email: email};
}
else if (username) {
if (AccountsTemplates.options.lowercaseUsername) {
username = toLowercaseUsername(username);
}
loginSelector = {username: username};
}
else {
if (AccountsTemplates.options.lowercaseUsername) {
username_and_email = toLowercaseUsername(username_and_email);
}
loginSelector = username_and_email;
}
Meteor.loginWithPassword(loginSelector, password, function(error) {
AccountsTemplates.submitCallback(error, state, function(){
AccountsTemplates.setState("signIn");
});
});
}
});
});
}
//----------------
// Forgot Password
//----------------
if (state === "forgotPwd"){
return Accounts.forgotPassword({
email: email
}, function(error) {
AccountsTemplates.submitCallback(error, state, function(){
AccountsTemplates.state.form.set("result", AccountsTemplates.texts.info.emailSent);
t.$("#at-field-email").val("");
});
});
}
//--------------------------------
// Reset Password / Enroll Account
//--------------------------------
if (state === "resetPwd" || state === "enrollAccount") {
var paramToken = AccountsTemplates.getparamToken();
return Accounts.resetPassword(paramToken, password, function(error) {
AccountsTemplates.submitCallback(error, state, function(){
var pwd_field_id;
if (state === "resetPwd")
AccountsTemplates.state.form.set("result", AccountsTemplates.texts.info.pwdReset);
else // Enroll Account
AccountsTemplates.state.form.set("result", AccountsTemplates.texts.info.pwdSet);
t.$("#at-field-password").val("");
if (AccountsTemplates.options.confirmPassword)
t.$("#at-field-password_again").val("");
});
});
}
//----------------
// Change Password
//----------------
if (state === "changePwd"){
return Accounts.changePassword(current_password, password, function(error) {
AccountsTemplates.submitCallback(error, state, function(){
AccountsTemplates.state.form.set("result", AccountsTemplates.texts.info.pwdChanged);
t.$("#at-field-current_password").val("");
t.$("#at-field-password").val("");
if (AccountsTemplates.options.confirmPassword)
t.$("#at-field-password_again").val("");
});
});
}
//----------------
// Resend Verification E-mail
//----------------
if (state === "resendVerificationEmail"){
return Meteor.call("ATResendVerificationEmail", email, function (error) {
AccountsTemplates.submitCallback(error, state, function(){
AccountsTemplates.state.form.set("result", AccountsTemplates.texts.info.verificationEmailSent);
t.$("#at-field-email").val("");
AccountsTemplates.avoidRedirect = true;
});
});
}
},
};

View file

@ -1,18 +0,0 @@
AT.prototype.atPwdFormBtnHelpers = {
submitDisabled: function(){
var disable = _.chain(AccountsTemplates.getFields())
.map(function(field){
return field.hasError() || field.isValidating();
})
.some()
.value()
;
if (disable)
return "disabled";
},
buttonText: function() {
var parentData = Template.currentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
return T9n.get(AccountsTemplates.texts.button[state], markIfMissing=false);
},
};

View file

@ -1,24 +0,0 @@
AT.prototype.atPwdLinkHelpers = {
disabled: function() {
return AccountsTemplates.disabled();
},
forgotPwdLink: function(){
return AccountsTemplates.getRoutePath("forgotPwd");
},
preText: function(){
return T9n.get(AccountsTemplates.texts.pwdLink_pre, markIfMissing=false);
},
linkText: function(){
return T9n.get(AccountsTemplates.texts.pwdLink_link, markIfMissing=false);
},
suffText: function(){
return T9n.get(AccountsTemplates.texts.pwdLink_suff, markIfMissing=false);
},
};
AT.prototype.atPwdLinkEvents = {
"click #at-forgotPwd": function(event, t) {
event.preventDefault();
AccountsTemplates.linkClick("forgotPwd");
},
};

View file

@ -1,19 +0,0 @@
AT.prototype.atReCaptchaRendered = function() {
$.getScript('//www.google.com/recaptcha/api.js?hl=' + T9n.getLanguage());
};
AT.prototype.atReCaptchaHelpers = {
key: function() {
if (AccountsTemplates.options.reCaptcha && AccountsTemplates.options.reCaptcha.siteKey)
return AccountsTemplates.options.reCaptcha.siteKey;
return Meteor.settings.public.reCaptcha.siteKey;
},
theme: function() {
return AccountsTemplates.options.reCaptcha && AccountsTemplates.options.reCaptcha.theme;
},
data_type: function() {
return AccountsTemplates.options.reCaptcha && AccountsTemplates.options.reCaptcha.data_type;
},
};

View file

@ -1,24 +0,0 @@
AT.prototype.atResendVerificationEmailLinkHelpers = {
disabled: function () {
return AccountsTemplates.disabled();
},
resendVerificationEmailLink: function () {
return AccountsTemplates.getRoutePath("resendVerificationEmail");
},
preText: function(){
return T9n.get(AccountsTemplates.texts.resendVerificationEmailLink_pre, markIfMissing=false);
},
linkText: function(){
return T9n.get(AccountsTemplates.texts.resendVerificationEmailLink_link, markIfMissing=false);
},
suffText: function(){
return T9n.get(AccountsTemplates.texts.resendVerificationEmailLink_suff, markIfMissing=false);
},
};
AT.prototype.atResendVerificationEmailLinkEvents = {
"click #at-resend-verification-email": function(event, t) {
event.preventDefault();
AccountsTemplates.linkClick('resendVerificationEmail');
},
};

View file

@ -1,7 +0,0 @@
AT.prototype.atResultHelpers = {
result: function() {
var resultText = AccountsTemplates.state.form.get("result");
if (resultText)
return T9n.get(resultText, markIfMissing=false);
},
};

View file

@ -1,5 +0,0 @@
AT.prototype.atSepHelpers = {
sepText: function(){
return T9n.get(AccountsTemplates.texts.sep, markIfMissing=false);
},
};

View file

@ -1,24 +0,0 @@
AT.prototype.atSigninLinkHelpers = {
disabled: function() {
return AccountsTemplates.disabled();
},
signInLink: function(){
return AccountsTemplates.getRoutePath("signIn");
},
preText: function(){
return T9n.get(AccountsTemplates.texts.signInLink_pre, markIfMissing=false);
},
linkText: function(){
return T9n.get(AccountsTemplates.texts.signInLink_link, markIfMissing=false);
},
suffText: function(){
return T9n.get(AccountsTemplates.texts.signInLink_suff, markIfMissing=false);
},
};
AT.prototype.atSigninLinkEvents = {
"click #at-signIn": function(event, t) {
event.preventDefault();
AccountsTemplates.linkClick("signIn");
},
};

View file

@ -1,24 +0,0 @@
AT.prototype.atSignupLinkHelpers = {
disabled: function() {
return AccountsTemplates.disabled();
},
signUpLink: function(){
return AccountsTemplates.getRoutePath("signUp");
},
preText: function(){
return T9n.get(AccountsTemplates.texts.signUpLink_pre, markIfMissing=false);
},
linkText: function(){
return T9n.get(AccountsTemplates.texts.signUpLink_link, markIfMissing=false);
},
suffText: function(){
return T9n.get(AccountsTemplates.texts.signUpLink_suff, markIfMissing=false);
},
};
AT.prototype.atSignupLinkEvents = {
"click #at-signUp": function(event, t) {
event.preventDefault();
AccountsTemplates.linkClick('signUp');
},
};

View file

@ -1,105 +0,0 @@
AT.prototype.atSocialHelpers = {
disabled: function() {
if (AccountsTemplates.disabled())
return "disabled";
var user = Meteor.user();
if (user){
var numServices = 0;
if (user.services)
numServices = _.keys(user.services).length; // including "resume"
if (numServices === 2 && user.services[this._id])
return "disabled";
}
},
name: function(){
return this._id;
},
iconClass: function() {
var ic = AccountsTemplates.texts.socialIcons[this._id];
if (!ic)
ic = "fa fa-" + this._id;
return ic;
},
buttonText: function() {
var service = this;
var serviceName = this._id;
if (serviceName === "meteor-developer")
serviceName = "meteor";
serviceName = capitalize(serviceName);
if (!service.configured)
return T9n.get(AccountsTemplates.texts.socialConfigure, markIfMissing=false) + " " + serviceName;
var showAddRemove = AccountsTemplates.options.showAddRemoveServices;
var user = Meteor.user();
if (user && showAddRemove){
if (user.services && user.services[this._id]){
var numServices = _.keys(user.services).length; // including "resume"
if (numServices === 2)
return serviceName;
else
return T9n.get(AccountsTemplates.texts.socialRemove, markIfMissing=false) + " " + serviceName;
} else
return T9n.get(AccountsTemplates.texts.socialAdd, markIfMissing=false) + " " + serviceName;
}
var parentData = Template.parentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
var prefix = state === "signIn" ?
T9n.get(AccountsTemplates.texts.socialSignIn, markIfMissing=false) :
T9n.get(AccountsTemplates.texts.socialSignUp, markIfMissing=false);
return prefix + " " + T9n.get(AccountsTemplates.texts.socialWith, markIfMissing=false) + " " + serviceName;
},
};
AT.prototype.atSocialEvents = {
"click button": function(event, t) {
event.preventDefault();
event.currentTarget.blur();
if (AccountsTemplates.disabled())
return;
var user = Meteor.user();
if (user && user.services && user.services[this._id]){
var numServices = _.keys(user.services).length; // including "resume"
if (numServices === 2)
return;
else{
AccountsTemplates.setDisabled(true);
Meteor.call("ATRemoveService", this._id, function(error){
AccountsTemplates.setDisabled(false);
});
}
} else {
AccountsTemplates.setDisabled(true);
var parentData = Template.parentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
var serviceName = this._id;
var methodName;
if (serviceName === 'meteor-developer')
methodName = "loginWithMeteorDeveloperAccount";
else
methodName = "loginWith" + capitalize(serviceName);
var loginWithService = Meteor[methodName];
options = {
loginStyle: AccountsTemplates.options.socialLoginStyle,
};
if (Accounts.ui) {
if (Accounts.ui._options.requestPermissions[serviceName]) {
options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName];
}
if (Accounts.ui._options.requestOfflineToken[serviceName]) {
options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName];
}
}
loginWithService(options, function(err) {
AccountsTemplates.setDisabled(false);
if (err && err instanceof Accounts.LoginCancelledError) {
// do nothing
}
else if (err && err instanceof ServiceConfiguration.ConfigError) {
if (Accounts._loginButtonsSession)
return Accounts._loginButtonsSession.configureService(serviceName);
}
else
AccountsTemplates.submitCallback(err, state);
});
}
},
};

View file

@ -1,33 +0,0 @@
AT.prototype.atTermsLinkHelpers = {
disabled: function() {
return AccountsTemplates.disabled();
},
text: function(){
return T9n.get(AccountsTemplates.texts.termsPreamble, markIfMissing=false);
},
privacyUrl: function(){
return AccountsTemplates.options.privacyUrl;
},
privacyLinkText: function(){
return T9n.get(AccountsTemplates.texts.termsPrivacy, markIfMissing=false);
},
showTermsAnd: function(){
return !!AccountsTemplates.options.privacyUrl && !!AccountsTemplates.options.termsUrl;
},
and: function(){
return T9n.get(AccountsTemplates.texts.termsAnd, markIfMissing=false);
},
termsUrl: function(){
return AccountsTemplates.options.termsUrl;
},
termsLinkText: function(){
return T9n.get(AccountsTemplates.texts.termsTerms, markIfMissing=false);
},
};
AT.prototype.atTermsLinkEvents = {
"click a": function(event) {
if (AccountsTemplates.disabled())
event.preventDefault();
},
};

View file

@ -1,7 +0,0 @@
AT.prototype.atTitleHelpers = {
title: function() {
var parentData = Template.currentData();
var state = (parentData && parentData.state) || AccountsTemplates.getState();
return T9n.get(AccountsTemplates.texts.title[state], markIfMissing = false);
},
};

View file

@ -1,12 +0,0 @@
<!-- Template level auth -->
<template name="ensureSignedIn">
{{#if signedIn}}
{{> Template.dynamic template=template}}
{{else}}
{{#if auth}}
{{> Template.dynamic template=auth}}
{{else}}
{{> fullPageAtForm}}
{{/if}}
{{/if}}
</template>

View file

@ -1,15 +0,0 @@
Template.ensureSignedIn.helpers({
signedIn: function () {
if (!Meteor.user()) {
AccountsTemplates.setState(AccountsTemplates.options.defaultState, function(){
var err = AccountsTemplates.texts.errors.mustBeLoggedIn;
AccountsTemplates.state.form.set('error', [err]);
});
return false;
} else {
AccountsTemplates.clearError();
return true;
}
}
});

View file

@ -1,19 +0,0 @@
capitalize = function(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
};
signedInAs = function() {
var user = Meteor.user();
if (user) {
if (user.username) {
return user.username;
} else if (user.profile && user.profile.name) {
return user.profile.name;
} else if (user.emails && user.emails[0]) {
return user.emails[0].address;
} else {
return "Signed In";
}
}
};

View file

@ -1,94 +0,0 @@
'use strict';
Package.describe({
summary: 'Meteor sign up and sign in templates core package.',
version: '1.14.2',
name: 'useraccounts:core',
git: 'https://github.com/meteor-useraccounts/core.git',
});
Package.onUse(function(api) {
//api.versionsFrom('METEOR@1.0.3');
api.use([
'accounts-base',
'check',
'underscore',
'reactive-var',
], ['client', 'server']);
api.use([
'blaze',
'reactive-dict',
'templating',
'jquery'
], 'client');
api.use([
'http'
], 'server');
api.imply([
'accounts-base',
'softwarerero:accounts-t9n@1.3.3',
], ['client', 'server']);
api.imply([
'templating',
], ['client']);
api.addFiles([
'lib/field.js',
'lib/core.js',
'lib/server.js',
'lib/methods.js',
'lib/server_methods.js',
], ['server']);
api.addFiles([
'lib/utils.js',
'lib/field.js',
'lib/core.js',
'lib/client.js',
'lib/templates_helpers/at_error.js',
'lib/templates_helpers/at_form.js',
'lib/templates_helpers/at_input.js',
'lib/templates_helpers/at_nav_button.js',
'lib/templates_helpers/at_oauth.js',
'lib/templates_helpers/at_pwd_form.js',
'lib/templates_helpers/at_pwd_form_btn.js',
'lib/templates_helpers/at_pwd_link.js',
'lib/templates_helpers/at_reCaptcha.js',
'lib/templates_helpers/at_resend_verification_email_link.js',
'lib/templates_helpers/at_result.js',
'lib/templates_helpers/at_sep.js',
'lib/templates_helpers/at_signin_link.js',
'lib/templates_helpers/at_signup_link.js',
'lib/templates_helpers/at_social.js',
'lib/templates_helpers/at_terms_link.js',
'lib/templates_helpers/at_title.js',
'lib/templates_helpers/at_message.js',
'lib/templates_helpers/ensure_signed_in.html',
'lib/templates_helpers/ensure_signed_in.js',
'lib/methods.js',
], ['client']);
api.export([
'AccountsTemplates',
], ['client', 'server']);
});
Package.onTest(function(api) {
api.use('useraccounts:core@1.14.2');
api.use([
'accounts-password',
'tinytest',
'test-helpers',
'underscore',
], ['client', 'server']);
api.addFiles([
'tests/tests.js',
], ['client', 'server']);
});

View file

@ -1,215 +0,0 @@
Tinytest.add("AccountsTemplates - addField/removeField", function(test) {
// Calls after AccountsTemplates.init()
AccountsTemplates._initialized = true;
test.throws(function() {
AccountsTemplates.addField("");
}, function(err) {
if (err instanceof Error && err.message === "AccountsTemplates.addField should strictly be called before AccountsTemplates.init!")
return true;
});
test.throws(function() {
AccountsTemplates.removeField("");
}, function(err) {
if (err instanceof Error && err.message === "AccountsTemplates.removeField should strictly be called before AccountsTemplates.init!")
return true;
});
AccountsTemplates._initialized = false;
// Trying to remove a non-existing field
test.throws(function() {
AccountsTemplates.removeField("foo");
}, function(err) {
if (err instanceof Error && err.message == "A field called foo does not exist!")
return true;
});
// Trying to remove an existing field
var email = AccountsTemplates.removeField("email");
test.isUndefined(AccountsTemplates.getField("email"));
// ...and puts it back in for tests re-run
AccountsTemplates.addField(email);
// Trying to add an already existing field
test.throws(function() {
var pwd = AccountsTemplates.getField("password");
AccountsTemplates.addField(pwd);
}, function(err) {
if (err instanceof Error && err.message == "A field called password already exists!")
return true;
});
var login = {
_id: "login",
displayName: "Email",
type: "email"
};
// Successful add
AccountsTemplates.addField(login);
// ...and removes it for tests re-run
AccountsTemplates.removeField("login");
// Invalid field.type
test.throws(function() {
AccountsTemplates.addField({
_id: "foo",
displayName: "Foo",
type: "bar"
});
}, function(err) {
if (err instanceof Error && err.message == "field.type is not valid!")
return true;
});
// Invalid minLength
test.throws(function() {
AccountsTemplates.addField({
_id: "first-name",
displayName: "First Name",
type: "text",
minLength: 0
});
}, function(err) {
if (err instanceof Error && err.message == "field.minLength should be greater than zero!")
return true;
});
// Invalid maxLength
test.throws(function() {
AccountsTemplates.addField({
_id: "first-name",
displayName: "First Name",
type: "text",
maxLength: 0
});
}, function(err) {
if (err instanceof Error && err.message == "field.maxLength should be greater than zero!")
return true;
});
// maxLength < minLength
test.throws(function() {
AccountsTemplates.addField({
_id: "first-name",
displayName: "First Name",
type: "text",
minLength: 2,
maxLength: 1
});
}, function(err) {
if (err instanceof Error && err.message == "field.maxLength should be greater than field.maxLength!")
return true;
});
// Successful add
var first_name = {
_id: "first_name",
displayName: "First Name",
type: "text",
minLength: 2,
maxLength: 50,
required: true
};
AccountsTemplates.addField(first_name);
// Now removes it to be consistent with tests re-run
AccountsTemplates.removeField("first_name");
});
Tinytest.add("AccountsTemplates - addFields", function(test) {
// Fake uninitialized state...
AccountsTemplates._initialized = false;
if (Meteor.isClient) {
// addFields does not exist client-side
test.throws(function() {
AccountsTemplates.addFields();
});
} else {
// Not an array of objects
test.throws(function() {
AccountsTemplates.addFields("");
}, function(err) {
if (err instanceof Error && err.message === "field argument should be an array of valid field objects!")
return true;
});
test.throws(function() {
AccountsTemplates.addFields(100);
}, function(err) {
if (err instanceof Error && err.message === "field argument should be an array of valid field objects!")
return true;
});
// Empty array
test.throws(function() {
AccountsTemplates.addFields([]);
}, function(err) {
if (err instanceof Error && err.message === "field argument should be an array of valid field objects!")
return true;
});
// Successful add
var first_name = {
_id: "first_name",
displayName: "First Name",
type: "text",
minLength: 2,
maxLength: 50,
required: true
};
var last_name = {
_id: "last_name",
displayName: "Last Name",
type: "text",
minLength: 2,
maxLength: 100,
required: false
};
AccountsTemplates.addFields([first_name, last_name]);
// Now removes ot to be consistend with tests re-run
AccountsTemplates.removeField("first_name");
AccountsTemplates.removeField("last_name");
}
// Restores initialized state...
AccountsTemplates._initialized = true;
});
Tinytest.add("AccountsTemplates - setState/getState", function(test) {
if (Meteor.isServer) {
// getState does not exist server-side
test.throws(function() {
AccountsTemplates.getState();
});
// setState does not exist server-side
test.throws(function() {
AccountsTemplates.setState();
});
} else {
_.each(AccountsTemplates.STATES, function(state){
AccountsTemplates.setState(state);
test.equal(AccountsTemplates.getState(), state);
});
// Setting an invalid state should throw a Meteor.Error
test.throws(function() {
AccountsTemplates.setState("foo");
}, function(err) {
if (err instanceof Meteor.Error && err.details == "accounts-templates-core package got an invalid state value!")
return true;
});
}
});
// -------------------------------------
// TODO: complite the following tests...
// -------------------------------------
Tinytest.add("AccountsTemplates - configure", function(test) {
if (Meteor.isClient) {
// configure does not exist client-side
test.throws(function() {
AccountsTemplates.configure({});
});
} else {
// TODO: write actual tests...
}
});

View file

@ -1,13 +0,0 @@
Package.describe({
name: 'meteorhacks:meteorx',
summary: 'Proxy for getting another meteorx fork',
version: '1.4.1'
});
Package.onUse((api) => {
api.export('MeteorX');
api.use([
'lamhieu:meteorx',
]);
});

View file

@ -81,7 +81,12 @@ Meteor.loginWithCas = function(options, callback) {
// check auth on server.
Accounts.callLoginMethod({
methodArguments: [{ cas: { credentialToken: credentialToken } }],
userCallback: callback
userCallback: err => {
// Fix redirect bug after login successfully
if (!err) {
window.location.href = '/';
}
}
});
}
}, 100);

View file

@ -71,14 +71,37 @@ class CAS {
callback({message: 'Empty response.'});
}
if (result['cas:serviceResponse']['cas:authenticationSuccess']) {
var userData = {
const userData = {
id: result['cas:serviceResponse']['cas:authenticationSuccess'][0]['cas:user'][0].toLowerCase(),
}
const attributes = result['cas:serviceResponse']['cas:authenticationSuccess'][0]['cas:attributes'][0];
for (var fieldName in attributes) {
userData[fieldName] = attributes[fieldName][0];
};
callback(undefined, true, userData);
const attributes = result['cas:serviceResponse']['cas:authenticationSuccess'][0]['cas:attributes'][0];
// Check allowed ldap groups if exist (array only)
// example cas settings : "allowedLdapGroups" : ["wekan", "admin"],
let findedGroup = false;
const allowedLdapGroups = Meteor.settings.cas.allowedLdapGroups || false;
for (const fieldName in attributes) {
if (allowedLdapGroups && fieldName === 'cas:memberOf') {
for (const groups in attributes[fieldName]) {
const str = attributes[fieldName][groups];
if (!Array.isArray(allowedLdapGroups)) {
callback({message: 'Settings "allowedLdapGroups" must be an array'});
}
for (const allowedLdapGroup in allowedLdapGroups) {
if (str.search(`cn=${allowedLdapGroups[allowedLdapGroup]}`) >= 0) {
findedGroup = true;
}
}
}
}
userData[fieldName] = attributes[fieldName][0];
}
if (allowedLdapGroups && !findedGroup) {
callback({message: 'Group not finded.'}, false);
} else {
callback(undefined, true, userData);
}
} else {
callback(undefined, false);
}
@ -229,13 +252,13 @@ const casValidate = (req, ticket, token, service, callback) => {
if (attrs.debug) {
console.log(`CAS response : ${JSON.stringify(result)}`);
}
let user = Users.findOne({ 'username': options.username });
let user = Meteor.users.findOne({ 'username': options.username });
if (! user) {
if (attrs.debug) {
console.log(`Creating user account ${JSON.stringify(options)}`);
}
const userId = Accounts.insertUserDoc({}, options);
user = Users.findOne(userId);
user = Meteor.users.findOne(userId);
}
if (attrs.debug) {
console.log(`Using user account ${JSON.stringify(user)}`);

View file

@ -2,20 +2,20 @@ Package.describe({
summary: "CAS support for accounts",
version: "0.1.0",
name: "wekan-accounts-cas",
git: "https://github.com/wekan/wekan-accounts-cas"
git: "https://github.com/wekan/meteor-accounts-cas"
});
Package.onUse(function(api) {
api.versionsFrom('METEOR@1.3.5.1');
api.versionsFrom('2.7');
api.use('routepolicy', 'server');
api.use('webapp', 'server');
api.use('accounts-base', ['client', 'server']);
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
api.use('underscore');
api.add_files('cas_client.js', 'web.browser');
api.add_files('cas_client_cordova.js', 'web.cordova');
api.add_files('cas_server.js', 'server');
api.addFiles('cas_client.js', 'web.browser');
api.addFiles('cas_client_cordova.js', 'web.cordova');
api.addFiles('cas_server.js', 'server');
});

View file

@ -9,7 +9,7 @@ Package.describe({
});
Package.onUse((api) => {
api.versionsFrom('1.4.2.3');
api.versionsFrom('2.7');
api.use([
'ecmascript',
'accounts-password',

View file

@ -7,11 +7,11 @@ Package.describe({
});
Package.onUse(function(api) {
api.use('accounts-base@1.2.0', ['client', 'server']);
api.use('accounts-base', ['client', 'server']);
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
api.use('accounts-oauth@1.1.0', ['client', 'server']);
api.use('wekan-oidc@1.0.10', ['client', 'server']);
api.use('accounts-oauth', ['client', 'server']);
api.use('wekan-oidc', ['client', 'server']);
api.addFiles('oidc_login_button.css', 'client');

View file

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

View file

@ -1,6 +1,5 @@
The MIT License (MIT)
Copyright (c) 2014-2019 The Wekan Team
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
@ -9,13 +8,14 @@ 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 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.
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.8.0",
name: "wekan-accounts-sandstorm",
git: "https://github.com/sandstorm-io/meteor-accounts-sandstorm.git"
});
Package.onUse(function(api) {
api.versionsFrom('2.7');
api.use('random', ['client', 'server']);
api.use('accounts-base', ['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);
}
});
});
}

View file

@ -12,9 +12,9 @@ Package.describe({
Package.onUse(function(api) {
api.versionsFrom('1.0.3.1');
api.use('yasaricli:slugify@0.0.5');
api.use('ecmascript@0.9.0');
api.versionsFrom('2.7');
api.use('yasaricli:slugify');
api.use('ecmascript');
api.use('underscore');
api.use('sha');
api.use('templating', 'client');

View file

@ -6,14 +6,14 @@ Package.describe({
});
Package.onUse(function(api) {
api.use('oauth2@1.1.0', ['client', 'server']);
api.use('oauth@1.1.0', ['client', 'server']);
api.use('http@1.1.0', ['server']);
api.use('underscore@1.0.0', 'client');
api.use('ecmascript@0.9.0');
api.use('templating@1.1.0', 'client');
api.use('random@1.0.0', 'client');
api.use('service-configuration@1.0.0', ['client', 'server']);
api.use('oauth2', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('http', ['server']);
api.use('underscore', 'client');
api.use('ecmascript');
api.use('templating', 'client');
api.use('random', 'client');
api.use('service-configuration', ['client', 'server']);
api.export('Oidc');