mirror of
https://github.com/wekan/wekan.git
synced 2026-02-20 23:14:07 +01:00
merge master changes
This commit is contained in:
commit
5df5c7f5d7
401 changed files with 142692 additions and 15007 deletions
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
2
packages/meteor-useraccounts-core/.gitignore
vendored
2
packages/meteor-useraccounts-core/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
.build*
|
||||
versions.json
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
client/compatibility
|
||||
packages
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
[](https://atmospherejs.com/useraccounts/core)
|
||||
[](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.
|
||||
|
||||
[](https://www.gittip.com/splendido/)
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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!");
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
AT.prototype.atMessageHelpers = {
|
||||
message: function() {
|
||||
var messageText = AccountsTemplates.state.form.get("message");
|
||||
if (messageText)
|
||||
return T9n.get(messageText, markIfMissing=false);
|
||||
},
|
||||
};
|
||||
|
|
@ -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");
|
||||
},
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
AT.prototype.atOauthHelpers = {
|
||||
oauthService: function() {
|
||||
return AccountsTemplates.oauthServices();
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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");
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
AT.prototype.atResultHelpers = {
|
||||
result: function() {
|
||||
var resultText = AccountsTemplates.state.form.get("result");
|
||||
if (resultText)
|
||||
return T9n.get(resultText, markIfMissing=false);
|
||||
},
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
AT.prototype.atSepHelpers = {
|
||||
sepText: function(){
|
||||
return T9n.get(AccountsTemplates.texts.sep, markIfMissing=false);
|
||||
},
|
||||
};
|
||||
|
|
@ -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");
|
||||
},
|
||||
};
|
||||
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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']);
|
||||
});
|
||||
|
|
@ -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...
|
||||
}
|
||||
});
|
||||
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)}`);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Package.describe({
|
|||
});
|
||||
|
||||
Package.onUse((api) => {
|
||||
api.versionsFrom('1.4.2.3');
|
||||
api.versionsFrom('2.7');
|
||||
api.use([
|
||||
'ecmascript',
|
||||
'accounts-password',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
3
packages/wekan-accounts-sandstorm/.gitignore
vendored
Normal file
3
packages/wekan-accounts-sandstorm/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.build*
|
||||
test-app/.meteor/local
|
||||
test-app/.meteor-spk
|
||||
|
|
@ -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.
|
||||
|
||||
137
packages/wekan-accounts-sandstorm/README.md
Normal file
137
packages/wekan-accounts-sandstorm/README.md
Normal 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()`.
|
||||
186
packages/wekan-accounts-sandstorm/client.js
Normal file
186
packages/wekan-accounts-sandstorm/client.js
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
45
packages/wekan-accounts-sandstorm/package.js
Normal file
45
packages/wekan-accounts-sandstorm/package.js
Normal 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");
|
||||
});
|
||||
210
packages/wekan-accounts-sandstorm/server.js
Normal file
210
packages/wekan-accounts-sandstorm/server.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue