Merge branch 'auth-change-password' into auth

Conflicts:
	packages/accounts-ui-unstyled/login_buttons.js
This commit is contained in:
Avital Oliver
2012-10-05 14:05:21 -07:00
5 changed files with 274 additions and 129 deletions

View File

@@ -1 +1 @@
Accounts.passwords = {};
Accounts.password = {};

View File

@@ -1,14 +1,11 @@
<template name="loginButtons">
<div id="login-buttons">
{{#if currentUser}}
<div class="login-header">
{{#if currentUser.loading}}
<div class="loading"></div>
{{else}}
{{displayName}}
{{/if}}
</div>
<div class="login-button" id="login-buttons-logout">Logout</div>
{{#if dropdown}}
{{> loginButtonsLoggedInDropdown}}
{{else}}
{{> loginButtonsLoggedInRow}}
{{/if}}
{{else}}
{{#if services}}
{{#if configurationLoaded}}
@@ -25,6 +22,44 @@
</div>
</template>
<template name="loginButtonsLoggedInDropdown">
<div class="login-link-and-dropdown-list">
{{#if currentUser.loading}}
<div class="loading"></div>
{{else}}
<a class="login-link-text" id="login-name-link">
{{displayName}} ▾
</a>
{{#if dropdownVisible}}
<div id="login-dropdown-list" class="accounts-dialog">
<a class="login-close-text">Close</a>
<div class="login-close-text-clear"></div>
{{#if inChangePasswordFlow}}
{{> loginButtonsChangePassword}}
{{else}}
{{#if allowChangingPassword}}
<div class="login-button" id="login-buttons-open-change-password">Change password</div>
{{/if}}
<div class="login-button" id="login-buttons-logout">Logout</div>
{{/if}}
</div>
{{/if}}
{{/if}}
</div>
</template>
<template name="loginButtonsLoggedInRow">
<div class="login-header">
{{#if currentUser.loading}}
<div class="loading"></div>
{{else}}
{{displayName}}
{{/if}}
</div>
<div class="login-button" id="login-buttons-logout">Logout</div>
</template>
<template name="loginButtonsServicesRow">
{{#each services}}
{{#if isPasswordService}}
@@ -70,12 +105,12 @@
{{#if configured}}
<div class="login-button" id="login-buttons-{{name}}">
<div class="login-image" id="login-buttons-image-{{name}}"></div>
Sign in with {{name}}
Sign in with {{capitalizedName}}
</div>
{{else}}
<div class="login-button configure-button" id="login-buttons-{{name}}">
<div class="login-image" id="login-buttons-image-{{name}}"></div>
Configure {{name}} Login
Configure {{capitalizedName}} Login
</div>
{{/if}}
{{/if}}
@@ -100,7 +135,7 @@
<template name="loginButtonsServicesDropdown">
<div class="login-link-and-dropdown-list">
<a class="login-link-text">Sign in</a>
<a class="login-link-text" id="login-sign-in-link">Sign in</a>
{{#if dropdownVisible}}
<div id="login-dropdown-list" class="accounts-dialog">
<a class="login-close-text">Close</a>
@@ -124,6 +159,18 @@
</div>
</template>
<template name="loginButtonsChangePassword">
{{#each fields}}
{{> loginButtonsFormField}}
{{/each}}
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-do-change-password">
Change password
</div>
</template>
<template name="resetPasswordForm">
{{#if inResetPasswordFlow}}
<div class="hide-background"></div>

View File

@@ -4,8 +4,12 @@
//
var DROPDOWN_VISIBLE_KEY = 'Meteor.loginButtons.dropdownVisible';
// XXX consider replacing these with one key that has an enum for values.
var IN_SIGNUP_FLOW_KEY = 'Meteor.loginButtons.inSignupFlow';
var IN_FORGOT_PASSWORD_FLOW_KEY = 'Meteor.loginButtons.inForgotPasswordFlow';
var IN_CHANGE_PASSWORD_FLOW_KEY = 'Meteor.loginButtons.inChangePasswordFlow';
var ERROR_MESSAGE_KEY = 'Meteor.loginButtons.errorMessage';
var INFO_MESSAGE_KEY = 'Meteor.loginButtons.infoMessage';
var RESET_PASSWORD_TOKEN_KEY = 'Meteor.loginButtons.resetPasswordToken';
@@ -19,6 +23,7 @@
var resetSession = function () {
Session.set(IN_SIGNUP_FLOW_KEY, false);
Session.set(IN_FORGOT_PASSWORD_FLOW_KEY, false);
Session.set(IN_CHANGE_PASSWORD_FLOW_KEY, false);
Session.set(DROPDOWN_VISIBLE_KEY, false);
resetMessages();
};
@@ -40,84 +45,72 @@
};
Template.loginButtons.events = {
'click #login-buttons-Facebook': function () {
'click #login-buttons-facebook': function () {
resetMessages();
Meteor.loginWithFacebook(function (e) {
if (!e || e instanceof Accounts.LoginCancelledError) {
// do nothing
} else if (e instanceof Accounts.ConfigError) {
configureService("Facebook"); // XXX refactor "Facebook" -> "facebook"
} else {
Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error");
}
});
Meteor.loginWithFacebook(makeLoginCallback('facebook'));
},
'click #login-buttons-Google': function () {
'click #login-buttons-google': function () {
resetMessages();
Meteor.loginWithGoogle(function (e) {
if (!e || e instanceof Accounts.LoginCancelledError) {
// do nothing
} else if (e instanceof Accounts.ConfigError) {
configureService("Google");
} else {
Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error");
}
});
Meteor.loginWithGoogle(makeLoginCallback('google'));
},
'click #login-buttons-Github': function () {
'click #login-buttons-github': function () {
resetMessages();
Meteor.loginWithGithub(function (e) {
if (!e || e instanceof Accounts.LoginCancelledError) {
// do nothing
} else if (e instanceof Accounts.ConfigError) {
configureService("Github");
} else {
Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error");
}
});
Meteor.loginWithGithub(makeLoginCallback('github'))
},
'click #login-buttons-Weibo': function () {
'click #login-buttons-weibo': function () {
resetMessages();
Meteor.loginWithWeibo(function (e) {
if (!e || e instanceof Accounts.LoginCancelledError) {
// do nothing
} else if (e instanceof Accounts.ConfigError) {
configureService("Weibo");
} else {
Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error");
}
});
Meteor.loginWithWeibo(makeLoginCallback('weibo'));
},
'click #login-buttons-Twitter': function () {
'click #login-buttons-twitter': function () {
resetMessages();
Meteor.loginWithTwitter(function (e) {
if (!e || e instanceof Accounts.LoginCancelledError) {
// do nothing
} else if (e instanceof Accounts.ConfigError) {
configureService("Twitter");
} else {
Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error");
}
});
Meteor.loginWithTwitter(makeLoginCallback('twitter'));
},
'click #login-name-link': function () {
Session.set(DROPDOWN_VISIBLE_KEY, true);
Meteor.flush();
correctDropdownZIndexes();
},
'click .login-close-text': function () {
resetSession();
},
'click #login-buttons-open-change-password': function() {
resetMessages();
Session.set(IN_CHANGE_PASSWORD_FLOW_KEY, true);
},
'click #login-buttons-logout': function() {
Meteor.logout();
resetSession();
Meteor.logout(resetSession);
}
};
var makeLoginCallback = function(service) {
return function (e) {
if (!e) {
resetSession();
} else if (e instanceof Accounts.LoginCancelledError) {
// do nothing
} else if (e instanceof Accounts.ConfigError) {
configureService(service);
} else {
Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error");
}
};
};
// decide whether we should show a dropdown rather than a row of
// buttons
Template.loginButtons.dropdown = function () {
var services = getLoginServices();
var hasPasswordService = _.any(services, function (service) {
return service.name === 'Password';
return service.name === 'password';
});
return hasPasswordService || services.length > 1;
@@ -131,19 +124,39 @@
return Accounts.loginServicesConfigured();
};
Template.loginButtons.displayName = function () {
//
// loginButtonsLoggedInRow template
//
Template.loginButtonsLoggedInRow.displayName = function () {
return displayName();
};
//
// loginButtonsLoggedInDropdown template
//
Template.loginButtonsLoggedInDropdown.displayName = function () {
return displayName();
};
Template.loginButtonsLoggedInDropdown.inChangePasswordFlow = function () {
return Session.get(IN_CHANGE_PASSWORD_FLOW_KEY);
};
Template.loginButtonsLoggedInDropdown.dropdownVisible = function () {
return Session.get(DROPDOWN_VISIBLE_KEY);
};
Template.loginButtonsLoggedInDropdown.allowChangingPassword = function () {
// it would be more correct to check whether the user has a password set,
// but in order to do that we'd have to send more data down to the client,
// and it'd be preferable not to send down the entire service.password document.
//
// instead we use the heuristic: if the user has a username or email set.
var user = Meteor.user();
if (!user)
return '';
if (user.profile && user.profile.name)
return user.profile.name;
if (user.username)
return user.username;
if (user.emails && user.emails[0] && user.emails[0].address)
return user.emails[0].address;
return '';
return user.username || (user.emails && user.emails[0] && user.emails[0].address);
};
@@ -275,7 +288,7 @@
};
Template.loginButtonsServicesRow.isPasswordService = function () {
return this.name === 'Password';
return this.name === 'password';
};
Template.loginButtonsServicesRow.hasOtherServices = function () {
@@ -303,6 +316,10 @@
return !!Accounts.configuration.findOne({service: this.name.toLowerCase()});
};
Template.loginButtonsServicesRow.capitalizedName = function () {
return capitalize(this.name);
};
//
// loginButtonsMessage template
@@ -352,23 +369,10 @@
//
Template.loginButtonsServicesDropdown.events = {
'click .login-link-text': function () {
'click #login-sign-in-link': function () {
Session.set(DROPDOWN_VISIBLE_KEY, true);
// IE <= 7 has a z-index bug that means we can't just give the
// dropdown a z-index and expect it to stack above the rest of
// the page even if nothing else has a z-index. The nature of
// the bug is that all positioned elements are considered to
// have z-index:0 (not auto) and therefore start new stacking
// contexts, with ties broken by page order.
//
// The fix, then is to give z-index:1 to all ancestors
// of the dropdown having z-index:0.
Meteor.flush();
for(var n = document.getElementById('login-dropdown-list').parentNode;
n.nodeName !== 'BODY';
n = n.parentNode)
if (n.style.zIndex === 0)
n.style.zIndex = 1;
correctDropdownZIndexes();
},
'click .login-close-text': function () {
resetSession();
@@ -499,6 +503,40 @@
}
});
//
// loginButtonsChangePassword template
//
Template.loginButtonsChangePassword.events({
'keypress #login-old-password, keypress #login-password, keypress #login-password-again': function (event) {
if (event.keyCode === 13)
changePassword();
},
'click #login-buttons-do-change-password': function () {
changePassword();
}
});
Template.loginButtonsChangePassword.fields = function () {
return [
{fieldName: 'old-password', fieldLabel: 'Current Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password', fieldLabel: 'New Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password-again', fieldLabel: 'New Password (again)',
inputType: 'password',
visible: function () {
return Meteor.accounts._options.requireUsername
&& !Meteor.accounts._options.requireEmail;
}}
];
};
//
// configureLoginServicesDialog template
//
@@ -510,7 +548,7 @@
'click #configure-login-services-dialog-save-configuration': function () {
if (Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED)) {
// Prepare the configuration document for this login service
var serviceName = Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME).toLowerCase();
var serviceName = Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME);
var configuration = {
service: serviceName
};
@@ -561,7 +599,7 @@
// template should be defined in the service's package
var configureLoginServicesDialogTemplateForService = function () {
var serviceName = Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME);
return Template['configureLoginServicesDialogFor' + serviceName];
return Template['configureLoginServicesDialogFor' + capitalize(serviceName)];
};
var configurationFields = function () {
@@ -590,6 +628,21 @@
// helpers
//
var displayName = function () {
var user = Meteor.user();
if (!user)
return '';
if (user.profile && user.profile.name)
return user.profile.name;
if (user.username)
return user.username;
if (user.emails && user.emails[0] && user.emails[0].address)
return user.emails[0].address;
return '';
};
var elementValueById = function(id) {
var element = document.getElementById(id);
if (!element)
@@ -619,6 +672,8 @@
Meteor.loginWithPassword(loginSelector, password, function (error, result) {
if (error) {
Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error");
} else {
resetSession();
}
});
};
@@ -650,13 +705,8 @@
else
options.password = password;
var passwordAgain = elementValueById('login-password-again');
if (passwordAgain !== null) {
if (password !== passwordAgain) {
Session.set(ERROR_MESSAGE_KEY, "Passwords don't match");
return;
}
}
if (!matchPasswordAgainIfPresent())
return;
if (Accounts._options.validateEmails)
options.validation = true;
@@ -664,6 +714,8 @@
Accounts.createUser(options, function (error) {
if (error) {
Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error");
} else {
resetSession();
}
});
};
@@ -675,25 +727,66 @@
login();
};
var changePassword = function () {
resetMessages();
var oldPassword = elementValueById('login-old-password');
var password = elementValueById('login-password');
if (!validatePassword(password))
return;
if (!matchPasswordAgainIfPresent())
return;
Accounts.changePassword(oldPassword, password, function (error) {
if (error) {
Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error");
} else {
Session.set(INFO_MESSAGE_KEY, "Password changed");
}
});
};
var matchPasswordAgainIfPresent = function () {
var passwordAgain = elementValueById('login-password-again');
if (passwordAgain !== null) {
var password = elementValueById('login-password');
if (password !== passwordAgain) {
Session.set(ERROR_MESSAGE_KEY, "Passwords don't match");
return false;
}
}
return true;
};
var correctDropdownZIndexes = function () {
// IE <= 7 has a z-index bug that means we can't just give the
// dropdown a z-index and expect it to stack above the rest of
// the page even if nothing else has a z-index. The nature of
// the bug is that all positioned elements are considered to
// have z-index:0 (not auto) and therefore start new stacking
// contexts, with ties broken by page order.
//
// The fix, then is to give z-index:1 to all ancestors
// of the dropdown having z-index:0.
for(var n = document.getElementById('login-dropdown-list').parentNode;
n.nodeName !== 'BODY';
n = n.parentNode)
if (n.style.zIndex === 0)
n.style.zIndex = 1;
};
var getLoginServices = function () {
var ret = [];
// XXX It would be nice if there were an automated way to read the
// list of services, such as _.each(Accounts.services, ...)
if (Accounts.facebook)
ret.push({name: 'Facebook'});
if (Accounts.google)
ret.push({name: 'Google'});
if (Accounts.weibo)
ret.push({name: 'Weibo'});
if (Accounts.twitter)
ret.push({name: 'Twitter'});
if (Accounts.github)
ret.push({name: 'Github'});
// make sure to put accounts last, since this is the order in the
// ui as well
if (Accounts.passwords)
ret.push({name: 'Password'});
// make sure to put password last, since this is how it is styled
// in the ui as well.
_.each(
['facebook', 'google', 'weibo', 'twitter', 'github', 'password'],
function (service) {
if (Accounts[service])
ret.push({name: service});
});
return ret;
};
@@ -727,5 +820,11 @@
return false;
}
};
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
var capitalize = function(str){
str = str == null ? '' : String(str);
return str.charAt(0).toUpperCase() + str.slice(1);
};
})();

View File

@@ -1,21 +1,21 @@
/* These should be in their respective packages */
#login-buttons-image-Google {
#login-buttons-image-google {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAADCklEQVQ4jSXSy2ucVRjA4d97zvdNJpPJbTJJE9rYaCINShZtRCFIA1bbLryBUlyoLQjqVl12W7UbN4qb1gtuYhFRRBCDBITaesFbbI3RFBLSptEY05l0ZjLfnMvrov/Bs3gAcF71x6VVHTk+o8nDH+hrH89rUK9Z9Yaen57S3wVtGaMBNGC0IegWKIDxTtVaOHVugZVmH3HX3Zz+4l+W1xvkOjuZfPsspY4CNkZELEgEIJKwYlBjEwjec/mfCMVuorVs76R8+P0KYMmP30U2dT8eIZqAR2ipRcWjEYxGSCRhV08e04oYMoxYLi97EI9YCJ0FHBYbIVGDlUBLwRlLIuYW6chEmQt/rJO09RJjhjEJEYvJYGNhkbUhw43OXtIWDFRq9G87nAaSK6sVRm8r8fzRMWbOX2Xx7ypd7ZET03sQhDOz73DqSJOrd+7HSo4QIu0Nx/4rOzx+cRXZ9+z7+uqJ+3hiepxK3fHZT2tMjXYzOtzL6dmznPzhLexgN0QlxAAYxAlqUqRmkf5j59RlNQ6MFHhgcpCTTx8EUb5e+plD7x4jjg1ANCAgrRQAdR7xKXjBlGyLYi7PxaUmb8z8xcpGHVXLHaXdjI0egKyJiQYTEhSPREVIEUBNC+Mqm+xpz3j0njLPHB2nsh1QgeG+IS48dYbD5YNoo0ZUAbVEuTUoKuBSZOarX/WhyQn6eg2+usDWf0s0tq8zNPYk+WI/Lnge++hlvlyfQ3NdECzGRWKwEEA0qNY251n69kV6+Y0kbaCZoebG2X3oU7pKoyxuXOPe945zs9DCeosGIXoBDyaLdf6ce4Hbk+/Y299ksKtAuaeNsiyw8c1LKIZ95b0MdgxA5giixACpTxEPSau6QdFfI5/2cLPmEW+JAQrtJUJzDXF1dkwHzVodJMX4HFEcQQMaFdPeM0Jb/4PUtzzaLKAhRyJFwo6lbegRNFfk819muV5dR4JBQoQdQ2xFiDmSNDHiaptamR9Gq5cQ18AledrGDpOfeI5Lq8u88smbhMRisoSAgAYghdfn5H/JkHuRZ1owLAAAAABJRU5ErkJggg==);
}
#login-buttons-image-Facebook {
#login-buttons-image-facebook {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQ4jWP8//8/AyWAhYGBgcEmauYZBgYGYxL1nj2yLN2ECcohVTNcDwsxKlXlhRm6yzwZRAS5GRgYGBhsombC5ZhwaUIGyJrRAVEuwGYzSS7AB/C64MiydKx8ZJfgNeDN+68MDAwIL8D4RLsgIHsJis0wPjKgOAyoE4hcnGwMGkpiBBUbacvA2TfuvaKiC759/3X23NUnOPMDtgTEwMBwloGBgYGR0uwMAGOPLJS9mkQHAAAAAElFTkSuQmCC);
}
#login-buttons-image-Weibo {
#login-buttons-image-weibo {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKySURBVDhPY2AgEpR5sjf/nS/6//UkoX+XJltuCvVxkcOp9cyZM1w/r13TuXvmDD9MkYIwg7qrNrubnzFb6J5intPHqrnvCnIwyKIYsmrVKuaFYWEFW2Sk79zX0f6/REHhKFABC0zRsky+rXMSeZdKCTLIHqgUvLAknW8L3IAQDw/RFlbWnQ801P+DNN8D4n0qyk94GRiEjTg5Lbz4+YOCdbhjVmTxbZwex7PUW58t8O1Ukf9gA2IDAoRPWFudfayt9f+mpsb/6yrK/28qKf4/ISf7YZu83K07QMNe6On9nyWusMtVm813azH/UWctZo/vc8TABjB3CApufAzSqKjw/7apyf+nMdH/XxUX/X+RnfX/qY/3/5tqqv/vq6v936KsfB2onltaiEHGx5AteFep4EmGUEHB1Adamv9v6er8fztp0v//79////nr1/+3X778B4N///5/O3jw/0N39//nlBQ/louLd4MMAWImcPhsU1G6DfLvt717wepnz537X0FB4T8fL+//AH///2/evgWL/7l///9dE+P/b4AWTZSWXg/UzAj2/w2gs59mZYEV7d+//z8rE9N/JUXF/w62tiD//a+urIS4BAgeA712Cxg2F40M36alpXGBDTgmI/3hdUU5WEFjff3/wvx8MNvcxARsQE1VFUQ30Et37Oz+P1RV+b/J0nIjUATigmgBvtzH5mb//9++/f/mkyf/A4KC/nv7+oI1W1hb/3/1+fP//9+//39ekP//CVDzTlnZxxtnz1ZBSUDeDAyZh7W13nybOeP/7W1b/09rbf2/FhgWHy9c+P912bL/D11d/l+WEP8/SUR4Ox8DA6pmmEkpHh4ya0JCim4lJGx7kZp8821CwrN7Hh4Pr7m6nDoSET61PjDQichsA3T7//+s/16/5gXSkIAa1AAAh8dhOVd5xHAAAAAASUVORK5CYII=);
}
#login-buttons-image-Twitter {
#login-buttons-image-twitter {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=);
}
#login-buttons-image-Github {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=);
}
}

File diff suppressed because one or more lines are too long